From 9621ada71ce8474961dcaf5be7ce56be54d40641 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 09:01:09 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 64 ++ skills/skill/SKILL.md | 516 +++++++++++ skills/skill/references/advanced-features.md | 689 +++++++++++++++ .../references/bindings-complete-guide.md | 665 ++++++++++++++ .../skill/references/development-patterns.md | 805 +++++++++++++++++ skills/skill/references/observability.md | 831 ++++++++++++++++++ .../references/wrangler-and-deployment.md | 713 +++++++++++++++ 9 files changed, 4298 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/skill/SKILL.md create mode 100644 skills/skill/references/advanced-features.md create mode 100644 skills/skill/references/bindings-complete-guide.md create mode 100644 skills/skill/references/development-patterns.md create mode 100644 skills/skill/references/observability.md create mode 100644 skills/skill/references/wrangler-and-deployment.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a7c4cde --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "cloudflare-workers", + "description": "Skill: Rapid development with Cloudflare Workers - build and deploy serverless applications", + "version": "0.0.0-2025.11.28", + "author": { + "name": "Misha Kolesnik", + "email": "misha@kolesnik.io" + }, + "skills": [ + "./skills/skill" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dba2c03 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# cloudflare-workers + +Skill: Rapid development with Cloudflare Workers - build and deploy serverless applications diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..5b9170f --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,64 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:tenequm/claude-plugins:cloudflare-workers", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "1b5be55e3b81c72e3235c2e59c4ae4fdcb8c68c5", + "treeHash": "d2135a3e51c274e35bd0ed1ed1fbebe565b3f7012ddd62e2806de01196aafb87", + "generatedAt": "2025-11-28T10:28:37.477653Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "cloudflare-workers", + "description": "Skill: Rapid development with Cloudflare Workers - build and deploy serverless applications" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "a99952c5c60d8b0ed879984f772e7a92988f43ee0e7a67d46401f798fafd316d" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "7241d12c4270529aa7d23f7747f49701768afc4f3c041b372b67c8bf56cb2daf" + }, + { + "path": "skills/skill/SKILL.md", + "sha256": "a1e92621222efd3640038bed9964a3ff9192c2a9625120ba741c07d7019e9c79" + }, + { + "path": "skills/skill/references/development-patterns.md", + "sha256": "697ce32a8ba7426e85e24f708fa73698c38aea52ea7603a2959de293d4c8cd34" + }, + { + "path": "skills/skill/references/wrangler-and-deployment.md", + "sha256": "72f6e3c3f4cce2338d2a1b72bbeffa48482bbab2b60c2d66745753ddc9857078" + }, + { + "path": "skills/skill/references/advanced-features.md", + "sha256": "bf43f99a51abfff6324501fe81aadcfd2d8d3d50305b2de7647aed9d1b1e31ca" + }, + { + "path": "skills/skill/references/bindings-complete-guide.md", + "sha256": "afbdfb2a1898054c9b439145bb9bb1bbf5037ff5c5847167eb55769d77f47f5c" + }, + { + "path": "skills/skill/references/observability.md", + "sha256": "c27bc2935af13a24e0300592e8d191a985e39f752af4201d2a9e78422cdaad61" + } + ], + "dirSha256": "d2135a3e51c274e35bd0ed1ed1fbebe565b3f7012ddd62e2806de01196aafb87" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/skill/SKILL.md b/skills/skill/SKILL.md new file mode 100644 index 0000000..75fc818 --- /dev/null +++ b/skills/skill/SKILL.md @@ -0,0 +1,516 @@ +--- +name: cloudflare-workers +description: Rapid development with Cloudflare Workers - build and deploy serverless applications on Cloudflare's global network. Use when building APIs, full-stack web apps, edge functions, background jobs, or real-time applications. Triggers on phrases like "cloudflare workers", "wrangler", "edge computing", "serverless cloudflare", "workers bindings", or files like wrangler.toml, worker.ts, worker.js. +--- + +# Cloudflare Workers + +## Overview + +Cloudflare Workers is a serverless execution environment that runs JavaScript, TypeScript, Python, and Rust code on Cloudflare's global network. Workers execute in milliseconds, scale automatically, and integrate with Cloudflare's storage and compute products through bindings. + +**Key Benefits:** +- **Zero cold starts** - Workers run in V8 isolates, not containers +- **Global deployment** - Code runs in 300+ cities worldwide +- **Rich ecosystem** - Bindings to D1, KV, R2, Durable Objects, Queues, and more +- **Full-stack capable** - Build APIs and serve static assets in one project +- **Standards-based** - Uses Web APIs (fetch, crypto, streams, WebSockets) + +## When to Use This Skill + +Use Cloudflare Workers for: + +- **APIs and backends** - RESTful APIs, GraphQL, tRPC, WebSocket servers +- **Full-stack applications** - React, Next.js, Remix, Astro, Vue, Svelte with static assets +- **Edge middleware** - Authentication, rate limiting, A/B testing, routing +- **Background processing** - Scheduled jobs (cron), queue consumers, webhooks +- **Data transformation** - ETL pipelines, real-time data processing +- **AI applications** - RAG systems, chatbots, image generation with Workers AI +- **Proxy and gateway** - API gateways, content transformation, protocol translation + +## Quick Start Workflow + +### 1. Install Wrangler CLI + +```bash +npm install -g wrangler + +# Login to Cloudflare +wrangler login +``` + +### 2. Create a New Worker + +```bash +# Using C3 (create-cloudflare) - recommended +npm create cloudflare@latest my-worker + +# Or create manually +wrangler init my-worker +cd my-worker +``` + +### 3. Write Your Worker + +**Basic HTTP API (TypeScript):** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === "/api/hello") { + return Response.json({ message: "Hello from Workers!" }); + } + + return new Response("Not found", { status: 404 }); + }, +}; +``` + +**With environment variables and KV:** + +```typescript +interface Env { + MY_VAR: string; + MY_KV: KVNamespace; +} + +export default { + async fetch(request: Request, env: Env): Promise { + // Access environment variable + const greeting = env.MY_VAR; + + // Read from KV + const value = await env.MY_KV.get("my-key"); + + return Response.json({ greeting, value }); + }, +}; +``` + +### 4. Develop Locally + +```bash +# Start local development server with hot reload +wrangler dev + +# Access at http://localhost:8787 +``` + +### 5. Deploy to Production + +```bash +# Deploy to workers.dev subdomain +wrangler deploy + +# Deploy to custom domain (configure in wrangler.toml) +wrangler deploy +``` + +## Core Concepts + +### Workers Runtime + +Workers use the V8 JavaScript engine with Web Standard APIs: + +- **Execution model**: Isolates (not containers) - instant cold starts +- **CPU time limit**: 10ms (Free), 30s (Paid) per request +- **Memory limit**: 128 MB per isolate +- **Languages**: JavaScript, TypeScript, Python, Rust +- **APIs**: fetch, crypto, streams, WebSockets, WebAssembly + +**Supported APIs:** +- Fetch API (HTTP requests) +- URL API (URL parsing) +- Web Crypto (encryption, hashing) +- Streams API (data streaming) +- WebSockets (real-time communication) +- Cache API (edge caching) +- HTML Rewriter (HTML transformation) + +### Handlers + +Workers respond to events through handlers: + +**Fetch Handler** (HTTP requests): +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext) { + return new Response("Hello!"); + }, +}; +``` + +**Scheduled Handler** (cron jobs): +```typescript +export default { + async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { + // Runs on schedule defined in wrangler.toml + await env.MY_KV.put("last-run", new Date().toISOString()); + }, +}; +``` + +**Queue Handler** (message processing): +```typescript +export default { + async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext) { + for (const message of batch.messages) { + await processMessage(message.body); + message.ack(); + } + }, +}; +``` + +### Bindings + +Bindings connect your Worker to Cloudflare resources. Configure in `wrangler.toml`: + +**KV (Key-Value Storage):** +```toml +[[kv_namespaces]] +binding = "MY_KV" +id = "your-kv-namespace-id" +``` + +```typescript +// Usage +await env.MY_KV.put("key", "value"); +const value = await env.MY_KV.get("key"); +``` + +**D1 (SQL Database):** +```toml +[[d1_databases]] +binding = "DB" +database_name = "my-database" +database_id = "your-database-id" +``` + +```typescript +// Usage +const result = await env.DB.prepare( + "SELECT * FROM users WHERE id = ?" +).bind(userId).all(); +``` + +**R2 (Object Storage):** +```toml +[[r2_buckets]] +binding = "MY_BUCKET" +bucket_name = "my-bucket" +``` + +```typescript +// Usage +await env.MY_BUCKET.put("file.txt", "contents"); +const object = await env.MY_BUCKET.get("file.txt"); +const text = await object?.text(); +``` + +**Environment Variables:** +```toml +[vars] +API_KEY = "development-key" # pragma: allowlist secret +``` + +**Secrets** (sensitive data): +```bash +# Set via CLI (not in wrangler.toml) +wrangler secret put API_KEY +``` + +### Context (ctx) + +The `ctx` parameter provides control over request lifecycle: + +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext) { + // Run tasks after response is sent + ctx.waitUntil( + env.MY_KV.put("request-count", String(Date.now())) + ); + + // Pass through to origin on exception + ctx.passThroughOnException(); + + return new Response("OK"); + }, +}; +``` + +## Rapid Development Patterns + +### Wrangler Configuration + +**Essential `wrangler.toml`:** + +```toml +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2025-01-01" + +# Custom domain +routes = [ + { pattern = "api.example.com/*", zone_name = "example.com" } +] + +# Or workers.dev subdomain +workers_dev = true + +# Environment variables +[vars] +ENVIRONMENT = "production" + +# Bindings +[[kv_namespaces]] +binding = "CACHE" +id = "your-kv-id" + +[[d1_databases]] +binding = "DB" +database_name = "production-db" +database_id = "your-db-id" + +[[r2_buckets]] +binding = "ASSETS" +bucket_name = "my-assets" + +# Cron triggers +[triggers] +crons = ["0 0 * * *"] # Daily at midnight +``` + +### Environment Management + +Use environments for staging/production: + +```toml +[env.staging] +vars = { ENVIRONMENT = "staging" } + +[env.staging.d1_databases] +binding = "DB" +database_name = "staging-db" +database_id = "staging-db-id" + +[env.production] +vars = { ENVIRONMENT = "production" } + +[env.production.d1_databases] +binding = "DB" +database_name = "production-db" +database_id = "production-db-id" +``` + +```bash +# Deploy to staging +wrangler deploy --env staging + +# Deploy to production +wrangler deploy --env production +``` + +### Common Patterns + +**JSON API with Error Handling:** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + try { + const url = new URL(request.url); + + if (url.pathname === "/api/users" && request.method === "GET") { + const users = await env.DB.prepare("SELECT * FROM users").all(); + return Response.json(users.results); + } + + if (url.pathname === "/api/users" && request.method === "POST") { + const body = await request.json(); + await env.DB.prepare( + "INSERT INTO users (name, email) VALUES (?, ?)" + ).bind(body.name, body.email).run(); + return Response.json({ success: true }, { status: 201 }); + } + + return Response.json({ error: "Not found" }, { status: 404 }); + } catch (error) { + return Response.json( + { error: error.message }, + { status: 500 } + ); + } + }, +}; +``` + +**Authentication Middleware:** + +```typescript +async function authenticate(request: Request, env: Env): Promise { + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return null; + } + + const token = authHeader.substring(7); + const userId = await env.SESSIONS.get(token); + return userId; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const userId = await authenticate(request, env); + + if (!userId) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Proceed with authenticated request + return Response.json({ userId }); + }, +}; +``` + +**CORS Headers:** + +```typescript +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +}; + +export default { + async fetch(request: Request): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + const response = await handleRequest(request); + + // Add CORS headers to response + Object.entries(corsHeaders).forEach(([key, value]) => { + response.headers.set(key, value); + }); + + return response; + }, +}; +``` + +### Static Assets (Full-Stack Apps) + +Serve static files alongside your Worker code: + +```toml +[assets] +directory = "./public" +binding = "ASSETS" +``` + +```typescript +import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // API routes + if (url.pathname.startsWith("/api/")) { + return handleAPI(request, env); + } + + // Serve static assets + try { + return await getAssetFromKV( + { request, waitUntil: () => {} }, + { ASSET_NAMESPACE: env.ASSETS } + ); + } catch { + return new Response("Not found", { status: 404 }); + } + }, +}; +``` + +### Testing + +**Using Vitest:** + +```typescript +import { env, createExecutionContext } from "cloudflare:test"; +import { describe, it, expect } from "vitest"; +import worker from "./index"; + +describe("Worker", () => { + it("responds with JSON", async () => { + const request = new Request("http://example.com/api/hello"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ message: "Hello!" }); + }); +}); +``` + +## Framework Integration + +Workers supports major frameworks with adapters: + +- **Next.js** - Full App Router and Pages Router support +- **Remix / React Router** - Native Cloudflare adapter +- **Astro** - Server-side rendering on Workers +- **SvelteKit** - Cloudflare adapter available +- **Hono** - Lightweight web framework built for Workers +- **tRPC** - Type-safe APIs with full Workers support + +**Example with Hono:** + +```typescript +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("/", (c) => c.text("Hello!")); +app.get("/api/users/:id", async (c) => { + const id = c.req.param("id"); + const user = await c.env.DB.prepare( + "SELECT * FROM users WHERE id = ?" + ).bind(id).first(); + return c.json(user); +}); + +export default app; +``` + +## Advanced Topics + +For detailed information on advanced features, see the reference files: + +- **Complete Bindings Guide**: `references/bindings-complete-guide.md` - Deep dive into all binding types (Durable Objects, Queues, Vectorize, AI, Hyperdrive, etc.) +- **Deployment & CI/CD**: `references/wrangler-and-deployment.md` - Wrangler commands, GitHub Actions, GitLab CI/CD, gradual rollouts, versions +- **Development Best Practices**: `references/development-patterns.md` - Testing strategies, debugging, error handling, performance optimization +- **Advanced Features**: `references/advanced-features.md` - Workers for Platforms, Smart Placement, WebSockets, streaming, custom domains +- **Observability**: `references/observability.md` - Logging (tail, Logpush, Workers Logs), metrics, traces, debugging + +## Resources + +**Official Documentation:** +- Workers: https://developers.cloudflare.com/workers/ +- Wrangler CLI: https://developers.cloudflare.com/workers/wrangler/ +- Runtime APIs: https://developers.cloudflare.com/workers/runtime-apis/ +- Examples: https://developers.cloudflare.com/workers/examples/ + +**Templates & Quick Starts:** +- Templates: https://developers.cloudflare.com/workers/get-started/quickstarts/ +- Framework guides: https://developers.cloudflare.com/workers/framework-guides/ + +**Community:** +- Discord: https://discord.cloudflare.com +- GitHub: https://github.com/cloudflare/workers-sdk diff --git a/skills/skill/references/advanced-features.md b/skills/skill/references/advanced-features.md new file mode 100644 index 0000000..20c94e8 --- /dev/null +++ b/skills/skill/references/advanced-features.md @@ -0,0 +1,689 @@ +# Advanced Features + +Advanced Workers capabilities for complex applications and enterprise use cases. + +## Workers for Platforms + +Deploy and manage customer-provided Workers on your infrastructure. + +**Use cases:** +- SaaS platforms where customers write custom code +- Low-code/no-code platforms +- Plugin systems +- Custom business logic hosting + +### Setup + +**wrangler.toml:** + +```toml +[[dispatch_namespaces]] +binding = "DISPATCHER" +namespace = "my-platform" + +# Optional outbound Worker +outbound = { service = "my-outbound-worker" } +``` + +### Dynamic Dispatch + +Route requests to customer Workers dynamically. + +**Dispatch Worker:** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const customerId = url.hostname.split(".")[0]; + + // Get customer's Worker + const userWorker = env.DISPATCHER.get(customerId); + + // Forward request to customer's Worker + return userWorker.fetch(request); + }, +}; +``` + +### Upload User Workers + +**Via API:** + +```typescript +async function uploadUserWorker( + customerId: string, + code: string, + env: Env +): Promise { + const response = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/workers/dispatch/namespaces/${env.NAMESPACE}/scripts/${customerId}`, + { + method: "PUT", + headers: { + "Authorization": `Bearer ${env.API_TOKEN}`, + "Content-Type": "application/javascript", + }, + body: code, + } + ); + + if (!response.ok) { + throw new Error(`Failed to upload: ${await response.text()}`); + } +} +``` + +**Via Wrangler:** + +```bash +wrangler dispatch-namespace put my-platform customer-123 ./customer-worker.js +``` + +### Outbound Workers + +Control what customer Workers can access. + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + // Validate request from customer Worker + const url = new URL(request.url); + + // Block certain domains + const blockedDomains = ["internal.example.com"]; + if (blockedDomains.some((d) => url.hostname.includes(d))) { + return new Response("Forbidden", { status: 403 }); + } + + // Add authentication + request.headers.set("X-Platform-Auth", env.PLATFORM_SECRET); + + // Forward to destination + return fetch(request); + }, +}; +``` + +### Limits and Quotas + +Set limits for customer Workers. + +```toml +[[dispatch_namespaces]] +binding = "DISPATCHER" +namespace = "my-platform" + +[dispatch_namespaces.outbound] +service = "outbound-worker" +parameters = { + cpu_ms = 50, + requests = 1000 +} +``` + +## Smart Placement + +Automatically place Workers near data sources to reduce latency. + +### Enable Smart Placement + +**wrangler.toml:** + +```toml +[placement] +mode = "smart" +``` + +**How it works:** +- Workers monitors where your Worker makes subrequests +- Automatically places future executions near those data sources +- Reduces round-trip time for database/API calls +- No code changes required + +**Best for:** +- Database-heavy Workers +- Workers making many external API calls +- Geographically distributed data sources + +**Limitations:** +- Not compatible with Durable Objects +- Requires Workers Standard plan + +## WebSockets + +Build real-time applications with WebSockets. + +### Basic WebSocket Server + +```typescript +export default { + async fetch(request: Request): Promise { + const upgradeHeader = request.headers.get("Upgrade"); + + if (upgradeHeader !== "websocket") { + return new Response("Expected WebSocket", { status: 426 }); + } + + const [client, server] = Object.values(new WebSocketPair()); + + // Handle WebSocket messages + server.accept(); + + server.addEventListener("message", (event) => { + console.log("Received:", event.data); + + // Echo message back + server.send(`Echo: ${event.data}`); + }); + + server.addEventListener("close", (event) => { + console.log("WebSocket closed:", event.code, event.reason); + }); + + server.addEventListener("error", (event) => { + console.error("WebSocket error:", event); + }); + + return new Response(null, { + status: 101, + webSocket: client, + }); + }, +}; +``` + +### WebSocket with Durable Objects + +Use Durable Objects for coordinated WebSocket servers. + +**Durable Object:** + +```typescript +export class ChatRoom { + state: DurableObjectState; + sessions: Set; + + constructor(state: DurableObjectState) { + this.state = state; + this.sessions = new Set(); + } + + async fetch(request: Request): Promise { + const [client, server] = Object.values(new WebSocketPair()); + + server.accept(); + this.sessions.add(server); + + server.addEventListener("message", (event) => { + // Broadcast to all connected clients + this.broadcast(event.data as string); + }); + + server.addEventListener("close", () => { + this.sessions.delete(server); + }); + + return new Response(null, { + status: 101, + webSocket: client, + }); + } + + broadcast(message: string) { + for (const session of this.sessions) { + try { + session.send(message); + } catch (error) { + // Remove failed sessions + this.sessions.delete(session); + } + } + } +} +``` + +**Worker:** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const roomId = url.pathname.substring(1) || "default"; + + // Get Durable Object for this room + const id = env.CHAT_ROOM.idFromName(roomId); + const room = env.CHAT_ROOM.get(id); + + return room.fetch(request); + }, +}; +``` + +### WebSocket Hibernation + +Reduce costs by hibernating idle WebSocket connections. + +```typescript +export class HibernatingChatRoom { + state: DurableObjectState; + + constructor(state: DurableObjectState) { + this.state = state; + + // Enable hibernation + state.setWebSocketAutoResponse( + new WebSocketRequestResponsePair("ping", "pong") + ); + } + + async fetch(request: Request): Promise { + const [client, server] = Object.values(new WebSocketPair()); + + // Accept with hibernation + this.state.acceptWebSocket(server); + + return new Response(null, { + status: 101, + webSocket: client, + }); + } + + async webSocketMessage(ws: WebSocket, message: string) { + // Called when message received (Worker woken up) + const data = JSON.parse(message); + + // Broadcast to all + this.state.getWebSockets().forEach((socket) => { + socket.send(message); + }); + } + + async webSocketClose(ws: WebSocket, code: number, reason: string) { + // Cleanup on close + ws.close(code, reason); + } +} +``` + +## Streaming + +Stream responses for large datasets. + +### Streaming JSON + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + + // Stream in background + (async () => { + try { + await writer.write(encoder.encode("[")); + + const results = await env.DB.prepare( + "SELECT * FROM large_table" + ).all(); + + for (let i = 0; i < results.results.length; i++) { + const item = results.results[i]; + await writer.write(encoder.encode(JSON.stringify(item))); + + if (i < results.results.length - 1) { + await writer.write(encoder.encode(",")); + } + } + + await writer.write(encoder.encode("]")); + await writer.close(); + } catch (error) { + await writer.abort(error); + } + })(); + + return new Response(readable, { + headers: { "Content-Type": "application/json" }, + }); + }, +}; +``` + +### Server-Sent Events (SSE) + +```typescript +export default { + async fetch(request: Request): Promise { + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + + // Send events in background + (async () => { + try { + for (let i = 0; i < 10; i++) { + await writer.write( + encoder.encode(`data: ${JSON.stringify({ count: i })}\n\n`) + ); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + await writer.close(); + } catch (error) { + await writer.abort(error); + } + })(); + + return new Response(readable, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + }); + }, +}; +``` + +## Custom Domains + +Configure custom domains for Workers. + +### Via Wrangler + +```toml +routes = [ + { pattern = "api.example.com/*", zone_name = "example.com" } +] +``` + +### Via Dashboard + +1. Navigate to Workers & Pages +2. Select your Worker +3. Go to Settings > Triggers +4. Add Custom Domain + +### Multiple Domains + +```toml +routes = [ + { pattern = "api.example.com/*", zone_name = "example.com" }, + { pattern = "api.other-domain.com/*", zone_name = "other-domain.com" } +] +``` + +## Static Assets + +Serve static files with Workers. + +### Configuration + +**wrangler.toml:** + +```toml +[assets] +directory = "./public" +binding = "ASSETS" + +# HTML handling +html_handling = "auto-trailing-slash" + +# 404 handling +not_found_handling = "single-page-application" +``` + +### Custom Asset Handling + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // API routes + if (url.pathname.startsWith("/api/")) { + return handleAPI(request, env); + } + + // Static assets + try { + const asset = await env.ASSETS.fetch(request); + + // Add custom headers + const response = new Response(asset.body, asset); + response.headers.set("X-Custom-Header", "value"); + + return response; + } catch { + return new Response("Not found", { status: 404 }); + } + }, +}; +``` + +### Framework Integration + +**Next.js:** + +```bash +npm create cloudflare@latest my-next-app -- --framework=next +``` + +**Remix:** + +```bash +npm create cloudflare@latest my-remix-app -- --framework=remix +``` + +**Astro:** + +```bash +npm create cloudflare@latest my-astro-app -- --framework=astro +``` + +## TCP Sockets + +Connect to external services via TCP. + +```typescript +export default { + async fetch(request: Request): Promise { + const socket = connect({ + hostname: "example.com", + port: 6379, // Redis + }); + + const writer = socket.writable.getWriter(); + const encoder = new TextEncoder(); + + // Send Redis command + await writer.write(encoder.encode("PING\r\n")); + + // Read response + const reader = socket.readable.getReader(); + const { value } = await reader.read(); + const response = new TextDecoder().decode(value); + + return Response.json({ response }); + }, +}; +``` + +## HTML Rewriter + +Transform HTML on the fly. + +```typescript +class LinkRewriter { + element(element: Element) { + const href = element.getAttribute("href"); + + if (href && href.startsWith("/")) { + // Make absolute + element.setAttribute("href", `https://example.com${href}`); + } + + // Add tracking + element.setAttribute("data-tracked", "true"); + } +} + +export default { + async fetch(request: Request): Promise { + const response = await fetch(request); + + return new HTMLRewriter() + .on("a", new LinkRewriter()) + .on("img", { + element(element) { + // Lazy load images + element.setAttribute("loading", "lazy"); + }, + }) + .transform(response); + }, +}; +``` + +## Scheduled Events (Cron) + +Run Workers on a schedule. + +**wrangler.toml:** + +```toml +[triggers] +crons = [ + "0 0 * * *", # Daily at midnight + "*/15 * * * *", # Every 15 minutes + "0 9 * * 1-5" # Weekdays at 9 AM +] +``` + +**Handler:** + +```typescript +export default { + async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { + console.log("Cron trigger:", event.cron); + + // Cleanup old data + await env.DB.prepare("DELETE FROM logs WHERE created_at < ?") + .bind(Date.now() - 30 * 24 * 60 * 60 * 1000) + .run(); + + // Send daily report + ctx.waitUntil(sendDailyReport(env)); + }, +}; +``` + +## Email Handler + +Process incoming emails. + +**wrangler.toml:** + +```toml +[email] +name = "support@example.com" +``` + +**Handler:** + +```typescript +export default { + async email(message: ForwardableEmailMessage, env: Env) { + console.log("From:", message.from); + console.log("Subject:", message.headers.get("subject")); + + // Forward to support team + await message.forward("support-team@example.com"); + + // Or process email + const rawEmail = await new Response(message.raw).text(); + await env.EMAILS.put(message.headers.get("message-id"), rawEmail); + }, +}; +``` + +## Tail Workers + +Monitor and log other Workers. + +**wrangler.toml:** + +```toml +[tail_consumers] +service = "my-logging-worker" +``` + +**Tail Worker:** + +```typescript +export default { + async tail(events: TraceItem[], env: Env) { + for (const event of events) { + if (event.outcome === "exception") { + // Log errors to external service + await fetch("https://logs.example.com", { + method: "POST", + body: JSON.stringify({ + scriptName: event.scriptName, + error: event.exceptions, + timestamp: event.event.request.timestamp, + }), + }); + } + } + }, +}; +``` + +## Multi-Region Deployments + +Deploy Workers to specific regions (Enterprise only). + +```toml +[placement] +mode = "regional" +regions = ["us-east", "eu-west"] +``` + +## Workers Analytics Engine + +Write custom metrics. + +```toml +[[analytics_engine_datasets]] +binding = "ANALYTICS" +``` + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const start = Date.now(); + const response = await handleRequest(request, env); + const duration = Date.now() - start; + + // Write custom metrics + env.ANALYTICS.writeDataPoint({ + blobs: [request.url, request.method, String(response.status)], + doubles: [duration], + indexes: [request.headers.get("user-agent") || "unknown"], + }); + + return response; + }, +}; +``` + +## Additional Resources + +- **Workers for Platforms**: https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/ +- **Smart Placement**: https://developers.cloudflare.com/workers/configuration/smart-placement/ +- **WebSockets**: https://developers.cloudflare.com/workers/runtime-apis/websockets/ +- **Static Assets**: https://developers.cloudflare.com/workers/static-assets/ +- **Scheduled Events**: https://developers.cloudflare.com/workers/configuration/cron-triggers/ diff --git a/skills/skill/references/bindings-complete-guide.md b/skills/skill/references/bindings-complete-guide.md new file mode 100644 index 0000000..9b44562 --- /dev/null +++ b/skills/skill/references/bindings-complete-guide.md @@ -0,0 +1,665 @@ +# Complete Bindings Guide + +Bindings are how Workers connect to Cloudflare resources and external services. They provide zero-latency access to storage, databases, queues, and other services. + +## Storage Bindings + +### KV (Key-Value Storage) + +Global, low-latency, eventually consistent key-value storage. + +**Best for:** +- Configuration data +- User sessions +- Cache +- Small objects (<25 MB) + +**Configuration:** + +```toml +[[kv_namespaces]] +binding = "MY_KV" +id = "your-namespace-id" +preview_id = "preview-namespace-id" # For local dev +``` + +**API:** + +```typescript +// Write +await env.MY_KV.put("key", "value"); +await env.MY_KV.put("key", "value", { + expirationTtl: 3600, // Expire in 1 hour + metadata: { user: "123" } +}); + +// Read +const value = await env.MY_KV.get("key"); +const json = await env.MY_KV.get("key", "json"); +const buffer = await env.MY_KV.get("key", "arrayBuffer"); + +// With metadata +const { value, metadata } = await env.MY_KV.getWithMetadata("key"); + +// Delete +await env.MY_KV.delete("key"); + +// List keys +const keys = await env.MY_KV.list({ prefix: "user:" }); +``` + +**Limits:** +- Key size: 512 bytes +- Value size: 25 MB +- Write rate: 1 write/second per key (eventually consistent) +- Read rate: unlimited + +### D1 (SQL Database) + +Serverless SQLite database built on SQLite. + +**Best for:** +- Structured data +- Relational data +- Complex queries +- ACID transactions + +**Configuration:** + +```toml +[[d1_databases]] +binding = "DB" +database_name = "my-database" +database_id = "your-database-id" +``` + +**API:** + +```typescript +// Query with bind parameters +const result = await env.DB.prepare( + "SELECT * FROM users WHERE id = ?" +).bind(userId).all(); + +// Insert +await env.DB.prepare( + "INSERT INTO users (name, email) VALUES (?, ?)" +).bind(name, email).run(); + +// Update +await env.DB.prepare( + "UPDATE users SET last_login = ? WHERE id = ?" +).bind(new Date().toISOString(), userId).run(); + +// Transaction +const results = await env.DB.batch([ + env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Alice"), + env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind("Bob"), +]); + +// First row only +const user = await env.DB.prepare( + "SELECT * FROM users WHERE id = ?" +).bind(userId).first(); +``` + +**Features:** +- Read replication (low latency reads globally) +- Time Travel (restore to any point in last 30 days) +- Backups +- Migrations via Wrangler + +**Limits:** +- Database size: 10 GB (Paid), 500 MB (Free) +- Rows read: 25M/day (Paid), 5M/day (Free) +- Rows written: 50M/day (Paid), 100K/day (Free) + +### R2 (Object Storage) + +S3-compatible object storage with zero egress fees. + +**Best for:** +- Large files +- Media storage +- Static assets +- Backups + +**Configuration:** + +```toml +[[r2_buckets]] +binding = "MY_BUCKET" +bucket_name = "my-bucket" +jurisdiction = "eu" # Optional: eu or fedramp +``` + +**API:** + +```typescript +// Put object +await env.MY_BUCKET.put("file.txt", "contents", { + httpMetadata: { + contentType: "text/plain", + cacheControl: "max-age=3600", + }, + customMetadata: { + user: "123", + }, +}); + +// Put from stream +await env.MY_BUCKET.put("large-file.bin", request.body); + +// Get object +const object = await env.MY_BUCKET.get("file.txt"); +if (object) { + const text = await object.text(); + const buffer = await object.arrayBuffer(); + const stream = object.body; // ReadableStream + + // Metadata + console.log(object.httpMetadata); + console.log(object.customMetadata); +} + +// Get with range +const object = await env.MY_BUCKET.get("file.txt", { + range: { offset: 0, length: 1024 } +}); + +// Head (metadata only) +const object = await env.MY_BUCKET.head("file.txt"); + +// Delete +await env.MY_BUCKET.delete("file.txt"); + +// List objects +const objects = await env.MY_BUCKET.list({ + prefix: "images/", + limit: 1000, +}); + +// Multipart upload (for large files) +const upload = await env.MY_BUCKET.createMultipartUpload("large.bin"); +const part = await upload.uploadPart(1, data); +await upload.complete([part]); +``` + +**Features:** +- Automatic multipart uploads +- Object versioning +- Event notifications +- Public buckets +- Custom domains + +**Limits:** +- Max object size: 5 TB +- Storage: unlimited +- Operations: unlimited + +## Compute Bindings + +### Durable Objects + +Strongly consistent, coordinated stateful objects with SQLite storage. + +**Best for:** +- Real-time collaboration +- WebSocket servers +- Coordination +- Strong consistency +- Per-user/per-room state + +**Configuration:** + +```toml +[[durable_objects.bindings]] +name = "COUNTER" +class_name = "Counter" +script_name = "my-worker" # Optional: if in different Worker + +[[migrations]] +tag = "v1" +new_classes = ["Counter"] +``` + +**Durable Object Class:** + +```typescript +export class Counter { + state: DurableObjectState; + + constructor(state: DurableObjectState, env: Env) { + this.state = state; + } + + async fetch(request: Request): Promise { + // Get from storage + let count = (await this.state.storage.get("count")) || 0; + + // Increment + count++; + + // Put to storage + await this.state.storage.put("count", count); + + return Response.json({ count }); + } + + // Alarms (scheduled actions) + async alarm() { + await this.state.storage.delete("count"); + } +} +``` + +**Worker Usage:** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + // Get Durable Object ID + const id = env.COUNTER.idFromName("global-counter"); + + // Get stub (reference) + const stub = env.COUNTER.get(id); + + // Call via fetch + return stub.fetch(request); + }, +}; +``` + +**Storage API:** + +```typescript +// Single operations +await this.state.storage.put("key", "value"); +const value = await this.state.storage.get("key"); +await this.state.storage.delete("key"); + +// Batch operations +await this.state.storage.put({ + key1: "value1", + key2: "value2", +}); + +const values = await this.state.storage.get(["key1", "key2"]); + +// List +const entries = await this.state.storage.list(); + +// SQL (new) +const result = await this.state.storage.sql.exec( + "SELECT * FROM users WHERE id = ?", userId +); +``` + +**Features:** +- SQLite-backed storage +- Automatic persistence +- Alarms (scheduled actions) +- WebSocket Hibernation +- Point-in-time recovery + +### Queues + +Message queuing for async processing with guaranteed delivery. + +**Best for:** +- Background jobs +- Async processing +- Decoupling services +- Retry logic + +**Configuration:** + +```toml +[[queues.producers]] +binding = "MY_QUEUE" +queue = "my-queue" + +[[queues.consumers]] +queue = "my-queue" +max_batch_size = 100 +max_batch_timeout = 30 +``` + +**Producer (send messages):** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + // Send single message + await env.MY_QUEUE.send({ userId: 123, action: "process" }); + + // Send batch + await env.MY_QUEUE.sendBatch([ + { body: { userId: 123 } }, + { body: { userId: 456 } }, + ]); + + return Response.json({ queued: true }); + }, +}; +``` + +**Consumer (receive messages):** + +```typescript +export default { + async queue(batch: MessageBatch, env: Env): Promise { + for (const message of batch.messages) { + try { + await processMessage(message.body); + message.ack(); // Mark as processed + } catch (error) { + message.retry(); // Retry later + } + } + }, +}; +``` + +**Features:** +- Guaranteed delivery +- Automatic retries +- Dead letter queues +- Batch processing +- Pull consumers (API-based) + +## AI & ML Bindings + +### Workers AI + +Run AI models directly from Workers. + +**Best for:** +- Text generation (LLMs) +- Embeddings +- Image generation +- Speech recognition +- Translation + +**Configuration:** + +```toml +[ai] +binding = "AI" +``` + +**API:** + +```typescript +// Text generation +const response = await env.AI.run("@cf/meta/llama-3-8b-instruct", { + messages: [ + { role: "system", content: "You are a helpful assistant" }, + { role: "user", content: "What is Cloudflare?" } + ], +}); + +// Embeddings +const embeddings = await env.AI.run("@cf/baai/bge-base-en-v1.5", { + text: "The quick brown fox jumps over the lazy dog", +}); + +// Image generation +const image = await env.AI.run("@cf/stabilityai/stable-diffusion-xl-base-1.0", { + prompt: "A sunset over the ocean", +}); + +// Speech to text +const result = await env.AI.run("@cf/openai/whisper", { + audio: audioData, +}); + +// Streaming +const stream = await env.AI.run("@cf/meta/llama-3-8b-instruct", { + messages: [{ role: "user", content: "Tell me a story" }], + stream: true, +}); +``` + +### Vectorize + +Vector database for similarity search. + +**Best for:** +- Semantic search +- RAG (Retrieval Augmented Generation) +- Recommendations +- Embeddings storage + +**Configuration:** + +```toml +[[vectorize]] +binding = "VECTORIZE" +index_name = "my-index" +``` + +**API:** + +```typescript +// Insert vectors +await env.VECTORIZE.insert([ + { id: "1", values: [0.1, 0.2, ...], metadata: { text: "..." } }, + { id: "2", values: [0.3, 0.4, ...], metadata: { text: "..." } }, +]); + +// Query (similarity search) +const results = await env.VECTORIZE.query( + [0.15, 0.25, ...], // Query vector + { + topK: 5, + returnMetadata: true, + } +); + +// With metadata filtering +const results = await env.VECTORIZE.query(vector, { + topK: 5, + filter: { category: "technology" }, +}); +``` + +## Database Bindings + +### Hyperdrive + +Accelerate access to existing databases via connection pooling and caching. + +**Best for:** +- Connecting to existing Postgres/MySQL +- Reducing latency to traditional databases +- Connection pooling + +**Configuration:** + +```toml +[[hyperdrive]] +binding = "HYPERDRIVE" +id = "your-hyperdrive-id" +``` + +**Usage with postgres:** + +```typescript +import { Client } from "pg"; + +const client = new Client({ + connectionString: env.HYPERDRIVE.connectionString, +}); + +await client.connect(); +const result = await client.query("SELECT * FROM users"); +await client.end(); +``` + +**Features:** +- Connection pooling +- Query caching +- Read replicas support + +## Service Bindings + +Call other Workers via RPC or HTTP. + +**Configuration:** + +```toml +[[services]] +binding = "AUTH_SERVICE" +service = "auth-worker" +environment = "production" +``` + +**HTTP-based:** + +```typescript +const response = await env.AUTH_SERVICE.fetch(new Request("http://auth/verify")); +``` + +**RPC-based (recommended):** + +```typescript +// In auth-worker +export class AuthService extends WorkerEntrypoint { + async verifyToken(token: string): Promise { + // Verify logic + return true; + } +} + +// In calling worker +const isValid = await env.AUTH_SERVICE.verifyToken(token); +``` + +## Additional Bindings + +### Analytics Engine + +Write custom analytics and metrics. + +```toml +[[analytics_engine_datasets]] +binding = "ANALYTICS" +``` + +```typescript +env.ANALYTICS.writeDataPoint({ + blobs: ["user-123", "click"], + doubles: [1.5], + indexes: ["button-1"], +}); +``` + +### Browser Rendering + +Control headless browsers. + +```toml +browser = { binding = "BROWSER" } +``` + +```typescript +const browser = await puppeteer.launch(env.BROWSER); +const page = await browser.newPage(); +await page.goto("https://example.com"); +const screenshot = await page.screenshot(); +``` + +### Rate Limiting + +Built-in rate limiting. + +```toml +[[unsafe.bindings]] +name = "RATE_LIMITER" +type = "ratelimit" +namespace_id = "your-namespace-id" +simple = { limit = 100, period = 60 } +``` + +```typescript +const { success } = await env.RATE_LIMITER.limit({ key: userId }); +if (!success) { + return new Response("Rate limited", { status: 429 }); +} +``` + +### mTLS + +Present client certificates. + +```toml +[[mtls_certificates]] +binding = "CERT" +certificate_id = "your-cert-id" +``` + +```typescript +const response = await fetch("https://api.example.com", { + certificate: env.CERT, +}); +``` + +## Best Practices + +### Binding Selection + +- **KV**: Configuration, sessions, cache +- **D1**: Structured data, complex queries +- **R2**: Large files, media, backups +- **Durable Objects**: Real-time, strong consistency, coordination +- **Queues**: Background jobs, async processing +- **Workers AI**: AI/ML inference +- **Vectorize**: Similarity search, RAG + +### Performance + +- Use `ctx.waitUntil()` for non-critical writes +- Batch operations when possible +- Use appropriate consistency models +- Cache frequently accessed data + +### Error Handling + +Always handle errors from bindings: + +```typescript +try { + const value = await env.MY_KV.get("key"); +} catch (error) { + // Handle error + console.error("KV error:", error); + return new Response("Service unavailable", { status: 503 }); +} +``` + +### Local Development + +Use Wrangler for local testing with bindings: + +```bash +# KV +wrangler kv:namespace create MY_KV --preview + +# D1 +wrangler d1 create my-database + +# Local dev with bindings +wrangler dev +``` + +## Additional Resources + +- **Bindings Reference**: https://developers.cloudflare.com/workers/runtime-apis/bindings/ +- **KV**: https://developers.cloudflare.com/kv/ +- **D1**: https://developers.cloudflare.com/d1/ +- **R2**: https://developers.cloudflare.com/r2/ +- **Durable Objects**: https://developers.cloudflare.com/durable-objects/ +- **Queues**: https://developers.cloudflare.com/queues/ +- **Workers AI**: https://developers.cloudflare.com/workers-ai/ +- **Vectorize**: https://developers.cloudflare.com/vectorize/ diff --git a/skills/skill/references/development-patterns.md b/skills/skill/references/development-patterns.md new file mode 100644 index 0000000..4af14a0 --- /dev/null +++ b/skills/skill/references/development-patterns.md @@ -0,0 +1,805 @@ +# Development Best Practices + +Patterns and best practices for building robust, maintainable Workers applications. + +## Testing + +### Vitest Integration + +Workers has first-class Vitest integration for unit and integration testing. + +**Setup:** + +```bash +npm install -D vitest @cloudflare/vitest-pool-workers +``` + +**vitest.config.ts:** + +```typescript +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.toml" }, + }, + }, + }, +}); +``` + +**Basic Test:** + +```typescript +import { env, createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; +import { describe, it, expect } from "vitest"; +import worker from "./index"; + +describe("Worker", () => { + it("responds with JSON", async () => { + const request = new Request("http://example.com/api/users"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data).toHaveProperty("users"); + }); + + it("handles errors gracefully", async () => { + const request = new Request("http://example.com/api/error"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + + expect(response.status).toBe(500); + }); +}); +``` + +**Testing with Bindings:** + +```typescript +import { env } from "cloudflare:test"; + +describe("KV operations", () => { + it("reads and writes to KV", async () => { + // env provides access to bindings configured in wrangler.toml + await env.MY_KV.put("test-key", "test-value"); + const value = await env.MY_KV.get("test-key"); + expect(value).toBe("test-value"); + }); +}); +``` + +**Testing Durable Objects:** + +```typescript +import { env, runInDurableObject } from "cloudflare:test"; + +describe("Counter Durable Object", () => { + it("increments counter", async () => { + await runInDurableObject(env.COUNTER, async (instance, state) => { + const request = new Request("http://do/increment"); + const response = await instance.fetch(request); + const data = await response.json(); + expect(data.count).toBe(1); + }); + }); +}); +``` + +**Run Tests:** + +```bash +npm test +# or +npx vitest +``` + +### Integration Testing + +Test full request/response cycles with external services. + +```typescript +describe("External API integration", () => { + it("fetches data from external API", async () => { + const request = new Request("http://example.com/api/external"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + + expect(response.status).toBe(200); + + // Verify external API was called correctly + const data = await response.json(); + expect(data).toHaveProperty("externalData"); + }); +}); +``` + +### Mocking + +Mock external dependencies for isolated tests. + +```typescript +import { vi } from "vitest"; + +describe("Mocked fetch", () => { + it("handles fetch errors", async () => { + // Mock global fetch + global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + const request = new Request("http://example.com/api/data"); + const ctx = createExecutionContext(); + const response = await worker.fetch(request, env, ctx); + + expect(response.status).toBe(503); + }); +}); +``` + +## Error Handling + +### Global Error Handling + +Catch all errors and return appropriate responses. + +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + try { + return await handleRequest(request, env, ctx); + } catch (error) { + console.error("Uncaught error:", error); + + return Response.json( + { + error: "Internal server error", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } + }, +}; + +async function handleRequest(request: Request, env: Env, ctx: ExecutionContext): Promise { + // Your handler logic +} +``` + +### Custom Error Classes + +Define custom error types for better error handling. + +```typescript +class NotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = "NotFoundError"; + } +} + +class UnauthorizedError extends Error { + constructor(message: string) { + super(message); + this.name = "UnauthorizedError"; + } +} + +class ValidationError extends Error { + constructor(public fields: Record) { + super("Validation failed"); + this.name = "ValidationError"; + } +} + +async function handleRequest(request: Request, env: Env): Promise { + try { + // Your logic + const user = await getUser(userId); + if (!user) { + throw new NotFoundError("User not found"); + } + + return Response.json(user); + } catch (error) { + if (error instanceof NotFoundError) { + return Response.json({ error: error.message }, { status: 404 }); + } + + if (error instanceof UnauthorizedError) { + return Response.json({ error: error.message }, { status: 401 }); + } + + if (error instanceof ValidationError) { + return Response.json( + { error: "Validation failed", fields: error.fields }, + { status: 400 } + ); + } + + // Unknown error + console.error("Unexpected error:", error); + return Response.json({ error: "Internal server error" }, { status: 500 }); + } +} +``` + +### Retry Logic + +Implement retry logic for transient failures. + +```typescript +async function fetchWithRetry( + url: string, + options: RequestInit = {}, + maxRetries = 3 +): Promise { + let lastError: Error; + + for (let i = 0; i < maxRetries; i++) { + try { + const response = await fetch(url, options); + + if (response.ok) { + return response; + } + + // Don't retry on client errors (4xx) + if (response.status >= 400 && response.status < 500) { + return response; + } + + lastError = new Error(`HTTP ${response.status}`); + } catch (error) { + lastError = error as Error; + } + + // Exponential backoff + if (i < maxRetries - 1) { + await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000)); + } + } + + throw lastError!; +} +``` + +## Performance Optimization + +### Caching Strategies + +Use the Cache API effectively. + +```typescript +async function handleCachedRequest(request: Request): Promise { + const cache = caches.default; + const cacheKey = new Request(request.url, request); + + // Try to get from cache + let response = await cache.match(cacheKey); + + if (!response) { + // Cache miss - fetch from origin + response = await fetchFromOrigin(request); + + // Cache successful responses + if (response.ok) { + response = new Response(response.body, { + ...response, + headers: { + ...Object.fromEntries(response.headers), + "Cache-Control": "public, max-age=3600", + }, + }); + + // Don't await - cache in background + ctx.waitUntil(cache.put(cacheKey, response.clone())); + } + } + + return response; +} +``` + +**Cache with custom keys:** + +```typescript +function getCacheKey(request: Request, userId?: string): Request { + const url = new URL(request.url); + + // Include user ID in cache key for personalized content + if (userId) { + url.searchParams.set("userId", userId); + } + + return new Request(url.toString(), request); +} +``` + +### Response Streaming + +Stream responses for large data. + +```typescript +export default { + async fetch(request: Request): Promise { + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + + // Stream data in background + (async () => { + try { + const data = await fetchLargeDataset(); + + for (const item of data) { + await writer.write(new TextEncoder().encode(JSON.stringify(item) + "\n")); + } + + await writer.close(); + } catch (error) { + await writer.abort(error); + } + })(); + + return new Response(readable, { + headers: { + "Content-Type": "application/x-ndjson", + "Transfer-Encoding": "chunked", + }, + }); + }, +}; +``` + +### Batching Operations + +Batch multiple operations for better performance. + +```typescript +// Bad: Sequential operations +for (const userId of userIds) { + await env.KV.get(`user:${userId}`); +} + +// Good: Batch operations +const users = await Promise.all( + userIds.map((id) => env.KV.get(`user:${id}`)) +); + +// Even better: Use batch APIs when available +const results = await env.DB.batch([ + env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(1), + env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(2), + env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(3), +]); +``` + +### Background Tasks + +Use `ctx.waitUntil()` for non-critical work. + +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + // Process request + const response = await handleRequest(request, env); + + // Log analytics in background (don't block response) + ctx.waitUntil( + env.ANALYTICS.writeDataPoint({ + blobs: [request.url, request.method], + doubles: [performance.now()], + }) + ); + + // Update cache in background + ctx.waitUntil(updateCache(request, response, env)); + + return response; + }, +}; +``` + +## Debugging + +### Console Logging + +Use console methods for debugging. + +```typescript +console.log("Info:", { userId, action }); +console.error("Error occurred:", error); +console.warn("Deprecated API used"); +console.debug("Debug info:", data); + +// Structured logging +console.log(JSON.stringify({ + level: "info", + timestamp: Date.now(), + userId, + action, +})); +``` + +### Wrangler Tail + +View real-time logs during development. + +```bash +# Tail logs +wrangler tail + +# Filter by status +wrangler tail --status error + +# Filter by method +wrangler tail --method POST + +# Pretty format +wrangler tail --format pretty +``` + +### Source Maps + +Enable source maps for better error traces. + +**tsconfig.json:** + +```json +{ + "compilerOptions": { + "sourceMap": true + } +} +``` + +**wrangler.toml:** + +```toml +upload_source_maps = true +``` + +### Local Debugging + +Use DevTools for debugging. + +```bash +# Start with inspector +wrangler dev --inspector +``` + +Then open `chrome://inspect` in Chrome and connect to the worker. + +### Breakpoints + +Set breakpoints in your code. + +```typescript +export default { + async fetch(request: Request): Promise { + debugger; // Execution will pause here + + const data = await fetchData(); + return Response.json(data); + }, +}; +``` + +## Code Organization + +### Router Pattern + +Organize routes cleanly. + +```typescript +interface Route { + pattern: URLPattern; + handler: (request: Request, env: Env, params: URLPatternResult) => Promise; +} + +const routes: Route[] = [ + { + pattern: new URLPattern({ pathname: "/api/users" }), + handler: handleGetUsers, + }, + { + pattern: new URLPattern({ pathname: "/api/users/:id" }), + handler: handleGetUser, + }, + { + pattern: new URLPattern({ pathname: "/api/users" }), + handler: handleCreateUser, + }, +]; + +export default { + async fetch(request: Request, env: Env): Promise { + for (const route of routes) { + const match = route.pattern.exec(request.url); + if (match) { + return route.handler(request, env, match); + } + } + + return Response.json({ error: "Not found" }, { status: 404 }); + }, +}; + +async function handleGetUsers(request: Request, env: Env): Promise { + const users = await env.DB.prepare("SELECT * FROM users").all(); + return Response.json(users.results); +} +``` + +### Middleware Pattern + +Chain middleware for cross-cutting concerns. + +```typescript +type Middleware = ( + request: Request, + env: Env, + next: () => Promise +) => Promise; + +const corsMiddleware: Middleware = async (request, env, next) => { + if (request.method === "OPTIONS") { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", + }, + }); + } + + const response = await next(); + response.headers.set("Access-Control-Allow-Origin", "*"); + return response; +}; + +const authMiddleware: Middleware = async (request, env, next) => { + const token = request.headers.get("Authorization"); + + if (!token) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Validate token + const isValid = await validateToken(token, env); + if (!isValid) { + return Response.json({ error: "Invalid token" }, { status: 401 }); + } + + return next(); +}; + +const loggingMiddleware: Middleware = async (request, env, next) => { + const start = Date.now(); + const response = await next(); + const duration = Date.now() - start; + + console.log({ + method: request.method, + url: request.url, + status: response.status, + duration, + }); + + return response; +}; + +function applyMiddleware( + handler: (request: Request, env: Env) => Promise, + middlewares: Middleware[] +): (request: Request, env: Env) => Promise { + return (request: Request, env: Env) => { + let index = -1; + + const dispatch = async (i: number): Promise => { + if (i <= index) { + throw new Error("next() called multiple times"); + } + + index = i; + + if (i === middlewares.length) { + return handler(request, env); + } + + const middleware = middlewares[i]; + return middleware(request, env, () => dispatch(i + 1)); + }; + + return dispatch(0); + }; +} + +// Usage +const handler = applyMiddleware( + async (request, env) => { + return Response.json({ message: "Hello!" }); + }, + [loggingMiddleware, corsMiddleware, authMiddleware] +); + +export default { fetch: handler }; +``` + +### Dependency Injection + +Use environment for dependencies. + +```typescript +interface Env { + DB: D1Database; + CACHE: KVNamespace; +} + +class UserService { + constructor(private env: Env) {} + + async getUser(id: string) { + // Try cache first + const cached = await this.env.CACHE.get(`user:${id}`); + if (cached) return JSON.parse(cached); + + // Fetch from database + const user = await this.env.DB.prepare( + "SELECT * FROM users WHERE id = ?" + ).bind(id).first(); + + // Update cache + if (user) { + await this.env.CACHE.put(`user:${id}`, JSON.stringify(user), { + expirationTtl: 3600, + }); + } + + return user; + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + const userService = new UserService(env); + const user = await userService.getUser("123"); + return Response.json(user); + }, +}; +``` + +## Security Best Practices + +### Input Validation + +Always validate and sanitize user input. + +```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().max(150), +}); + +export default { + async fetch(request: Request, env: Env): Promise { + try { + const body = await request.json(); + const validated = userSchema.parse(body); + + // Use validated data + await createUser(validated, env); + + return Response.json({ success: true }); + } catch (error) { + if (error instanceof z.ZodError) { + return Response.json( + { error: "Validation failed", issues: error.errors }, + { status: 400 } + ); + } + + throw error; + } + }, +}; +``` + +### Rate Limiting + +Implement rate limiting to prevent abuse. + +```typescript +async function checkRateLimit( + identifier: string, + env: Env, + limit = 100, + window = 60 +): Promise { + const key = `ratelimit:${identifier}`; + const current = await env.CACHE.get(key); + + if (!current) { + await env.CACHE.put(key, "1", { expirationTtl: window }); + return true; + } + + const count = parseInt(current); + + if (count >= limit) { + return false; + } + + await env.CACHE.put(key, String(count + 1), { expirationTtl: window }); + return true; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const ip = request.headers.get("CF-Connecting-IP") || "unknown"; + + const allowed = await checkRateLimit(ip, env); + + if (!allowed) { + return Response.json({ error: "Rate limit exceeded" }, { status: 429 }); + } + + return handleRequest(request, env); + }, +}; +``` + +### CSRF Protection + +Protect against cross-site request forgery. + +```typescript +function generateCSRFToken(): string { + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return btoa(String.fromCharCode(...array)); +} + +async function validateCSRFToken( + token: string, + sessionId: string, + env: Env +): Promise { + const stored = await env.SESSIONS.get(`csrf:${sessionId}`); + return stored === token; +} + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.method === "POST") { + const sessionId = request.headers.get("X-Session-ID"); + const csrfToken = request.headers.get("X-CSRF-Token"); + + if (!sessionId || !csrfToken) { + return Response.json({ error: "Missing tokens" }, { status: 403 }); + } + + const isValid = await validateCSRFToken(csrfToken, sessionId, env); + + if (!isValid) { + return Response.json({ error: "Invalid CSRF token" }, { status: 403 }); + } + } + + return handleRequest(request, env); + }, +}; +``` + +## Additional Resources + +- **Testing**: https://developers.cloudflare.com/workers/testing/ +- **Observability**: https://developers.cloudflare.com/workers/observability/ +- **Best Practices**: https://developers.cloudflare.com/workers/best-practices/ +- **Examples**: https://developers.cloudflare.com/workers/examples/ diff --git a/skills/skill/references/observability.md b/skills/skill/references/observability.md new file mode 100644 index 0000000..03daec3 --- /dev/null +++ b/skills/skill/references/observability.md @@ -0,0 +1,831 @@ +# Observability + +Monitoring, logging, and debugging Workers in production. + +## Logging + +### Real-time Logs (Wrangler Tail) + +View logs in real-time during development and production. + +```bash +# Tail all logs +wrangler tail + +# Tail specific environment +wrangler tail --env production + +# Filter by status +wrangler tail --status error +wrangler tail --status ok + +# Filter by HTTP method +wrangler tail --method POST + +# Filter by header +wrangler tail --header "User-Agent: Chrome" + +# Filter by IP +wrangler tail --ip 203.0.113.1 + +# Search in logs +wrangler tail --search "database error" + +# Sample rate (% of requests) +wrangler tail --sampling-rate 0.1 # 10% + +# Pretty format +wrangler tail --format pretty +``` + +### Console Logging + +Use console methods in your Worker. + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + // Log levels + console.log("Info message", { userId: 123 }); + console.info("Info message"); + console.warn("Warning message"); + console.error("Error message", error); + console.debug("Debug message", { data }); + + // Structured logging + console.log(JSON.stringify({ + level: "info", + timestamp: Date.now(), + message: "Request processed", + userId: 123, + duration: 45, + })); + + return new Response("OK"); + }, +}; +``` + +### Workers Logs + +Persistent logs stored in Cloudflare (Enterprise feature). + +**Enable via Dashboard:** +1. Workers & Pages → Logs +2. Enable Workers Logs +3. Set retention period (1-30 days) + +**Query logs:** + +```bash +# Via GraphQL API +curl -X POST https://api.cloudflare.com/client/v4/graphql \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "query": "query { viewer { accounts(filter: {accountTag: \"$ACCOUNT_ID\"}) { workersLogsData(filter: {datetime_gt: \"2025-01-01T00:00:00Z\"}) { logs { timestamp message } } } } }" + }' +``` + +**Filter logs:** + +```typescript +// Add custom fields for filtering +console.log(JSON.stringify({ + level: "error", + service: "api", + endpoint: "/users", + userId: "123", + error: error.message, +})); +``` + +### Logpush Integration + +Stream logs to external services. + +**Supported destinations:** +- Amazon S3 +- Google Cloud Storage +- Azure Blob Storage +- Datadog +- Splunk +- New Relic +- HTTP endpoint + +**Setup via Dashboard:** +1. Logs → Workers Logs → Create Job +2. Select destination +3. Configure filters +4. Set fields to include + +**Fields available:** +- `timestamp` - Request timestamp +- `level` - Log level (log, error, warn, info, debug) +- `message` - Log message +- `scriptName` - Worker name +- `outcome` - Request outcome (ok, exception, exceededCpu, etc.) +- `logs` - Array of console.log() messages + +### Custom Logging Service + +Send logs to your own service. + +```typescript +interface LogEntry { + level: "info" | "warn" | "error"; + message: string; + timestamp: number; + metadata?: Record; +} + +class Logger { + constructor(private env: Env) {} + + private async send(entry: LogEntry) { + // Send to logging service + await fetch("https://logs.example.com/ingest", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.env.LOG_TOKEN}`, + }, + body: JSON.stringify(entry), + }); + } + + info(message: string, metadata?: Record) { + const entry: LogEntry = { + level: "info", + message, + timestamp: Date.now(), + metadata, + }; + + console.log(JSON.stringify(entry)); + this.send(entry).catch(console.error); + } + + error(message: string, error: Error, metadata?: Record) { + const entry: LogEntry = { + level: "error", + message, + timestamp: Date.now(), + metadata: { + ...metadata, + error: error.message, + stack: error.stack, + }, + }; + + console.error(JSON.stringify(entry)); + this.send(entry).catch(console.error); + } +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const logger = new Logger(env); + + try { + logger.info("Request received", { + url: request.url, + method: request.method, + }); + + const response = await handleRequest(request, env); + + logger.info("Request completed", { + status: response.status, + }); + + return response; + } catch (error) { + logger.error("Request failed", error as Error, { + url: request.url, + }); + + throw error; + } + }, +}; +``` + +## Metrics and Analytics + +### Workers Analytics + +View request metrics in the dashboard. + +**Available metrics:** +- Requests (count, rate) +- Errors (count, rate) +- Success rate +- CPU time (p50, p99) +- Duration (p50, p99) +- Subrequests + +**Filter by:** +- Time range +- Status code +- Path +- User agent +- Country + +### GraphQL Analytics API + +Query analytics programmatically. + +```typescript +const query = ` + query { + viewer { + accounts(filter: {accountTag: "${accountId}"}) { + workersInvocationsAdaptive( + filter: { + datetime_gt: "2025-01-01T00:00:00Z" + datetime_lt: "2025-01-02T00:00:00Z" + scriptName: "my-worker" + } + limit: 100 + ) { + sum { + requests + errors + subrequests + } + quantiles { + cpuTimeP50 + cpuTimeP99 + durationP50 + durationP99 + } + } + } + } + } +`; + +const response = await fetch("https://api.cloudflare.com/client/v4/graphql", { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query }), +}); + +const data = await response.json(); +``` + +### Custom Metrics (Analytics Engine) + +Write custom metrics to Analytics Engine. + +**Configuration:** + +```toml +[[analytics_engine_datasets]] +binding = "ANALYTICS" +``` + +**Write data points:** + +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const start = Date.now(); + + try { + const response = await handleRequest(request, env); + const duration = Date.now() - start; + + // Write metrics + ctx.waitUntil( + env.ANALYTICS.writeDataPoint({ + // String fields (up to 20) + blobs: [ + request.url, + request.method, + String(response.status), + request.headers.get("user-agent") || "unknown", + ], + // Numeric fields (up to 20) + doubles: [ + duration, + response.headers.get("content-length") + ? parseInt(response.headers.get("content-length")!) + : 0, + ], + // Indexed fields (up to 20) - for filtering + indexes: [ + request.cf?.country as string || "unknown", + request.cf?.colo as string || "unknown", + ], + }) + ); + + return response; + } catch (error) { + const duration = Date.now() - start; + + ctx.waitUntil( + env.ANALYTICS.writeDataPoint({ + blobs: [request.url, request.method, "error"], + doubles: [duration], + indexes: ["error"], + }) + ); + + throw error; + } + }, +}; +``` + +**Query metrics:** + +```sql +SELECT + blob1 AS url, + blob2 AS method, + blob3 AS status, + COUNT() AS requests, + AVG(double1) AS avg_duration, + MAX(double1) AS max_duration +FROM ANALYTICS_DATASET +WHERE + timestamp >= NOW() - INTERVAL '1' DAY + AND index1 = 'US' +GROUP BY blob1, blob2, blob3 +ORDER BY requests DESC +LIMIT 100 +``` + +## Traces (OpenTelemetry) + +Export traces to observability platforms. + +**Supported platforms:** +- Datadog +- New Relic +- Honeycomb +- Grafana Cloud +- Sentry + +### Export to Honeycomb + +```typescript +import { trace } from "@opentelemetry/api"; +import { WorkersSDK } from "@cloudflare/workers-honeycomb-logger"; + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const sdk = new WorkersSDK(request, env, ctx, { + apiKey: env.HONEYCOMB_API_KEY, + dataset: "my-worker", + }); + + const tracer = trace.getTracer("my-worker"); + + return tracer.startActiveSpan("fetch", async (span) => { + try { + span.setAttribute("http.method", request.method); + span.setAttribute("http.url", request.url); + + const response = await handleRequest(request, env); + + span.setAttribute("http.status_code", response.status); + span.end(); + + return response; + } catch (error) { + span.recordException(error as Error); + span.end(); + throw error; + } + }); + }, +}; +``` + +### Export to Datadog + +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const startTime = Date.now(); + + try { + const response = await handleRequest(request, env); + const duration = Date.now() - startTime; + + // Send trace to Datadog + ctx.waitUntil( + fetch("https://http-intake.logs.datadoghq.com/v1/input", { + method: "POST", + headers: { + "Content-Type": "application/json", + "DD-API-KEY": env.DATADOG_API_KEY, + }, + body: JSON.stringify({ + ddsource: "cloudflare-workers", + service: "my-worker", + message: "Request completed", + duration, + status: response.status, + url: request.url, + method: request.method, + }), + }) + ); + + return response; + } catch (error) { + // Log error to Datadog + ctx.waitUntil( + fetch("https://http-intake.logs.datadoghq.com/v1/input", { + method: "POST", + headers: { + "Content-Type": "application/json", + "DD-API-KEY": env.DATADOG_API_KEY, + }, + body: JSON.stringify({ + ddsource: "cloudflare-workers", + service: "my-worker", + status: "error", + error: { + message: (error as Error).message, + stack: (error as Error).stack, + }, + }), + }) + ); + + throw error; + } + }, +}; +``` + +## Error Tracking + +### Error Boundaries + +Catch and track errors globally. + +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + try { + return await handleRequest(request, env, ctx); + } catch (error) { + // Log error + console.error("Unhandled error:", error); + + // Send to error tracking service + ctx.waitUntil(reportError(error as Error, request, env)); + + // Return error response + return Response.json( + { + error: "Internal server error", + requestId: crypto.randomUUID(), + }, + { status: 500 } + ); + } + }, +}; + +async function reportError(error: Error, request: Request, env: Env) { + await fetch("https://errors.example.com/report", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${env.ERROR_TOKEN}`, + }, + body: JSON.stringify({ + error: { + message: error.message, + stack: error.stack, + name: error.name, + }, + request: { + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers), + }, + timestamp: Date.now(), + }), + }); +} +``` + +### Sentry Integration + +```bash +npm install @sentry/browser +``` + +```typescript +import * as Sentry from "@sentry/browser"; + +export default { + async fetch(request: Request, env: Env): Promise { + Sentry.init({ + dsn: env.SENTRY_DSN, + environment: env.ENVIRONMENT, + }); + + try { + return await handleRequest(request, env); + } catch (error) { + Sentry.captureException(error); + throw error; + } + }, +}; +``` + +## Performance Monitoring + +### Request Timing + +Track request performance. + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const timings = { + start: Date.now(), + auth: 0, + database: 0, + external: 0, + total: 0, + }; + + // Auth + const authStart = Date.now(); + const user = await authenticate(request, env); + timings.auth = Date.now() - authStart; + + // Database + const dbStart = Date.now(); + const data = await env.DB.prepare("SELECT * FROM users WHERE id = ?") + .bind(user.id) + .first(); + timings.database = Date.now() - dbStart; + + // External API + const apiStart = Date.now(); + const externalData = await fetch("https://api.example.com/data"); + timings.external = Date.now() - apiStart; + + timings.total = Date.now() - timings.start; + + // Log timings + console.log("Performance:", timings); + + // Add to response headers + return Response.json(data, { + headers: { + "X-Timing-Auth": String(timings.auth), + "X-Timing-Database": String(timings.database), + "X-Timing-External": String(timings.external), + "X-Timing-Total": String(timings.total), + }, + }); + }, +}; +``` + +### Performance API + +Use the Performance API for detailed timing. + +```typescript +export default { + async fetch(request: Request): Promise { + performance.mark("start"); + + performance.mark("db-start"); + await queryDatabase(); + performance.mark("db-end"); + performance.measure("database", "db-start", "db-end"); + + performance.mark("api-start"); + await fetchExternal(); + performance.mark("api-end"); + performance.measure("external", "api-start", "api-end"); + + performance.mark("end"); + performance.measure("total", "start", "end"); + + // Get measurements + const measurements = performance.getEntriesByType("measure"); + + console.log("Performance measurements:", measurements); + + return Response.json({ ok: true }); + }, +}; +``` + +## Debugging + +### Local Debugging + +Debug Workers locally with DevTools. + +```bash +# Start with inspector +wrangler dev --inspector + +# Connect Chrome DevTools +# Open chrome://inspect in Chrome +# Click "inspect" on your Worker +``` + +**Features:** +- Set breakpoints +- Step through code +- Inspect variables +- View console logs +- Profile performance + +### Remote Debugging + +Debug production Workers. + +**Using console.log:** + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + console.log("Request:", { + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers), + }); + + const response = await handleRequest(request, env); + + console.log("Response:", { + status: response.status, + headers: Object.fromEntries(response.headers), + }); + + return response; + }, +}; +``` + +**View logs:** + +```bash +wrangler tail --format pretty +``` + +### Source Maps + +Enable source maps for better error traces. + +**tsconfig.json:** + +```json +{ + "compilerOptions": { + "sourceMap": true + } +} +``` + +**wrangler.toml:** + +```toml +upload_source_maps = true +``` + +### Debugging Tips + +1. **Use structured logging** - JSON format for easier parsing +2. **Log request IDs** - Track requests across services +3. **Time operations** - Identify performance bottlenecks +4. **Test locally first** - Use `wrangler dev` before deploying +5. **Use staging environment** - Test in production-like environment +6. **Monitor after deploy** - Watch logs and metrics after deployment + +## Alerting + +### Custom Alerts + +Send alerts based on metrics. + +```typescript +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + try { + const response = await handleRequest(request, env); + + // Alert on slow requests + const duration = Date.now() - startTime; + if (duration > 5000) { + ctx.waitUntil(sendAlert("Slow request", { duration, url: request.url }, env)); + } + + return response; + } catch (error) { + // Alert on errors + ctx.waitUntil(sendAlert("Request error", { error: error.message }, env)); + throw error; + } + }, +}; + +async function sendAlert(message: string, data: any, env: Env) { + // Send to Slack + await fetch(env.SLACK_WEBHOOK, { + method: "POST", + body: JSON.stringify({ + text: `🚨 ${message}`, + blocks: [ + { + type: "section", + text: { type: "mrkdwn", text: `*${message}*` }, + }, + { + type: "section", + text: { type: "mrkdwn", text: `\`\`\`${JSON.stringify(data, null, 2)}\`\`\`` }, + }, + ], + }), + }); +} +``` + +## Health Checks + +Implement health check endpoints. + +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + if (url.pathname === "/health") { + return healthCheck(env); + } + + return handleRequest(request, env); + }, +}; + +async function healthCheck(env: Env): Promise { + const checks = { + database: false, + kv: false, + external: false, + }; + + // Check database + try { + await env.DB.prepare("SELECT 1").first(); + checks.database = true; + } catch (error) { + console.error("Database check failed:", error); + } + + // Check KV + try { + await env.MY_KV.get("health-check"); + checks.kv = true; + } catch (error) { + console.error("KV check failed:", error); + } + + // Check external API + try { + const response = await fetch("https://api.example.com/health", { + signal: AbortSignal.timeout(2000), + }); + checks.external = response.ok; + } catch (error) { + console.error("External API check failed:", error); + } + + const allHealthy = Object.values(checks).every((c) => c); + + return Response.json( + { healthy: allHealthy, checks }, + { status: allHealthy ? 200 : 503 } + ); +} +``` + +## Additional Resources + +- **Observability**: https://developers.cloudflare.com/workers/observability/ +- **Logs**: https://developers.cloudflare.com/workers/observability/logs/ +- **Metrics**: https://developers.cloudflare.com/workers/observability/metrics-and-analytics/ +- **Traces**: https://developers.cloudflare.com/workers/observability/traces/ +- **Dev Tools**: https://developers.cloudflare.com/workers/observability/dev-tools/ diff --git a/skills/skill/references/wrangler-and-deployment.md b/skills/skill/references/wrangler-and-deployment.md new file mode 100644 index 0000000..7314250 --- /dev/null +++ b/skills/skill/references/wrangler-and-deployment.md @@ -0,0 +1,713 @@ +# Wrangler and Deployment Guide + +Wrangler is the official CLI for developing, testing, and deploying Cloudflare Workers. + +## Installation + +```bash +# NPM (global) +npm install -g wrangler + +# NPM (project-local) +npm install --save-dev wrangler + +# Verify installation +wrangler --version +``` + +## Authentication + +```bash +# Login via browser (recommended) +wrangler login + +# Or use API token +export CLOUDFLARE_API_TOKEN=your-token +``` + +## Essential Commands + +### Project Initialization + +```bash +# Initialize new project +wrangler init my-worker + +# With template +wrangler init my-worker --template cloudflare/workers-sdk + +# Interactive setup with C3 +npm create cloudflare@latest my-worker +``` + +### Development + +```bash +# Start local dev server +wrangler dev + +# Custom port +wrangler dev --port 8080 + +# With remote resources (bindings) +wrangler dev --remote + +# Local mode (no network requests to Cloudflare) +wrangler dev --local + +# Test worker (experimental) +wrangler dev --test-scheduled +``` + +### Deployment + +```bash +# Deploy to production +wrangler deploy + +# Deploy to specific environment +wrangler deploy --env staging +wrangler deploy --env production + +# Dry run (validate without deploying) +wrangler deploy --dry-run + +# Deploy specific file +wrangler deploy src/index.ts + +# Deploy with message +wrangler deploy --message "Fix authentication bug" +``` + +### Version Management + +```bash +# List versions +wrangler versions list + +# View specific version +wrangler versions view + +# Deploy specific version +wrangler versions deploy + +# Rollback to previous version +wrangler rollback + +# Gradual rollout +wrangler versions deploy --percentage 10 +``` + +## Configuration (wrangler.toml) + +### Basic Structure + +```toml +#:schema node_modules/wrangler/config-schema.json +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2025-01-01" + +# Account/Zone (usually auto-detected) +account_id = "your-account-id" + +# Workers.dev subdomain +workers_dev = true + +# Or custom domain +routes = [ + { pattern = "example.com/*", zone_name = "example.com" }, + { pattern = "api.example.com/*", zone_name = "example.com" } +] +``` + +### Environment Variables + +```toml +# Non-sensitive variables +[vars] +ENVIRONMENT = "production" +API_ENDPOINT = "https://api.example.com" +FEATURE_FLAGS = '{"newUI": true}' + +# Per-environment +[env.staging.vars] +ENVIRONMENT = "staging" +API_ENDPOINT = "https://staging-api.example.com" +``` + +### Secrets + +```bash +# Set secret via CLI (not in wrangler.toml!) +wrangler secret put API_KEY +# Enter value when prompted + +# List secrets +wrangler secret list + +# Delete secret +wrangler secret delete API_KEY + +# Bulk import from .env +wrangler secret bulk .env.production +``` + +### Bindings Configuration + +**KV:** +```toml +[[kv_namespaces]] +binding = "MY_KV" +id = "your-namespace-id" +preview_id = "preview-namespace-id" +``` + +**D1:** +```toml +[[d1_databases]] +binding = "DB" +database_name = "production-db" +database_id = "xxxx-xxxx-xxxx" +``` + +**R2:** +```toml +[[r2_buckets]] +binding = "MY_BUCKET" +bucket_name = "my-bucket" +preview_bucket_name = "my-bucket-preview" +``` + +**Durable Objects:** +```toml +[[durable_objects.bindings]] +name = "COUNTER" +class_name = "Counter" +script_name = "my-worker" + +[[migrations]] +tag = "v1" +new_classes = ["Counter"] +``` + +**Queues:** +```toml +[[queues.producers]] +binding = "MY_QUEUE" +queue = "my-queue" + +[[queues.consumers]] +queue = "my-queue" +max_batch_size = 10 +max_batch_timeout = 30 +max_retries = 3 +dead_letter_queue = "my-dlq" +``` + +**Service Bindings:** +```toml +[[services]] +binding = "AUTH_SERVICE" +service = "auth-worker" +environment = "production" +``` + +**Workers AI:** +```toml +[ai] +binding = "AI" +``` + +### Cron Triggers + +```toml +[triggers] +crons = [ + "0 0 * * *", # Daily at midnight + "*/15 * * * *", # Every 15 minutes + "0 9 * * 1-5" # Weekdays at 9 AM +] +``` + +### Static Assets + +```toml +[assets] +directory = "./public" +binding = "ASSETS" + +# HTML handling +html_handling = "auto-trailing-slash" # or "drop-trailing-slash", "none" + +# Not found handling +not_found_handling = "single-page-application" # or "404-page", "none" +``` + +### Compatibility + +```toml +# Compatibility date (required) +compatibility_date = "2025-01-01" + +# Compatibility flags +compatibility_flags = [ + "nodejs_compat", + "transformstream_enable_standard_constructor" +] +``` + +### Custom Builds + +```toml +[build] +command = "npm run build" +watch_dirs = ["src", "public"] + +[build.upload] +format = "modules" +dir = "dist" +main = "./index.js" +``` + +## Multi-Environment Setup + +### Environment Structure + +```toml +# Global settings +name = "my-worker" +main = "src/index.ts" +compatibility_date = "2025-01-01" + +# Default/production +[vars] +ENVIRONMENT = "production" + +[[kv_namespaces]] +binding = "CACHE" +id = "prod-kv-id" + +# Staging environment +[env.staging] +name = "my-worker-staging" +vars = { ENVIRONMENT = "staging" } + +[[env.staging.kv_namespaces]] +binding = "CACHE" +id = "staging-kv-id" + +# Development environment +[env.dev] +name = "my-worker-dev" +vars = { ENVIRONMENT = "development" } + +[[env.dev.kv_namespaces]] +binding = "CACHE" +id = "dev-kv-id" +``` + +### Deploying Environments + +```bash +# Deploy to production (default) +wrangler deploy + +# Deploy to staging +wrangler deploy --env staging + +# Deploy to dev +wrangler deploy --env dev +``` + +## CI/CD Integration + +### GitHub Actions + +**.github/workflows/deploy.yml:** + +```yaml +name: Deploy Worker + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: npm ci + + - name: Deploy to Cloudflare Workers + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} +``` + +**Multi-environment:** + +```yaml +name: Deploy Workers + +on: + push: + branches: [main, staging] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: npm ci + + - name: Deploy to staging + if: github.ref == 'refs/heads/staging' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + environment: 'staging' + + - name: Deploy to production + if: github.ref == 'refs/heads/main' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} +``` + +### GitLab CI/CD + +**.gitlab-ci.yml:** + +```yaml +stages: + - deploy + +deploy_production: + stage: deploy + image: node:18 + script: + - npm ci + - npx wrangler deploy + only: + - main + variables: + CLOUDFLARE_API_TOKEN: $CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID: $CLOUDFLARE_ACCOUNT_ID +``` + +## Workers Builds (Git Integration) + +Enable automatic deployments on git push via the dashboard. + +**Setup:** +1. Connect your GitHub/GitLab repository +2. Configure build settings +3. Set environment variables +4. Enable branch deployments + +**Benefits:** +- Automatic builds on push +- Preview deployments for PRs +- Build caching +- Deployment history + +## Versioning & Gradual Deployments + +### Versions + +Workers automatically create versions on each deployment. + +```bash +# List versions +wrangler versions list + +# View specific version +wrangler versions view + +# Deploy specific version +wrangler versions deploy +``` + +### Gradual Rollouts + +Incrementally deploy new versions to reduce risk. + +**Via Wrangler:** + +```bash +# Deploy to 10% of traffic +wrangler versions deploy --percentage 10 + +# Increase to 50% +wrangler versions deploy --percentage 50 + +# Full rollout +wrangler versions deploy --percentage 100 +``` + +**Via Configuration:** + +```toml +[[workflows.deployments]] +version_id = "new-version-id" +percentage = 10 + +[[workflows.deployments]] +version_id = "old-version-id" +percentage = 90 +``` + +### Rollback + +```bash +# Rollback to previous version +wrangler rollback + +# List rollback history +wrangler rollback --list + +# Rollback to specific version +wrangler versions deploy +``` + +## Resource Management + +### KV Namespaces + +```bash +# Create namespace +wrangler kv:namespace create "MY_KV" +wrangler kv:namespace create "MY_KV" --preview + +# List namespaces +wrangler kv:namespace list + +# Delete namespace +wrangler kv:namespace delete --namespace-id= + +# Put key-value +wrangler kv:key put "key" "value" --namespace-id= + +# Get value +wrangler kv:key get "key" --namespace-id= + +# List keys +wrangler kv:key list --namespace-id= + +# Delete key +wrangler kv:key delete "key" --namespace-id= + +# Bulk operations +wrangler kv:bulk put data.json --namespace-id= +wrangler kv:bulk delete keys.json --namespace-id= +``` + +### D1 Databases + +```bash +# Create database +wrangler d1 create my-database + +# List databases +wrangler d1 list + +# Execute SQL +wrangler d1 execute my-database --command="SELECT * FROM users" + +# Execute from file +wrangler d1 execute my-database --file=schema.sql + +# Migrations +wrangler d1 migrations create my-database "add-users-table" +wrangler d1 migrations apply my-database +wrangler d1 migrations list my-database + +# Export database +wrangler d1 export my-database --output=backup.sql + +# Time Travel (restore) +wrangler d1 time-travel restore my-database --timestamp= +``` + +### R2 Buckets + +```bash +# Create bucket +wrangler r2 bucket create my-bucket + +# List buckets +wrangler r2 bucket list + +# Delete bucket +wrangler r2 bucket delete my-bucket + +# Put object +wrangler r2 object put my-bucket/file.txt --file=./file.txt + +# Get object +wrangler r2 object get my-bucket/file.txt --file=./downloaded.txt + +# List objects +wrangler r2 object list my-bucket + +# Delete object +wrangler r2 object delete my-bucket/file.txt +``` + +### Queues + +```bash +# Create queue +wrangler queues create my-queue + +# List queues +wrangler queues list + +# Delete queue +wrangler queues delete my-queue + +# Send test message +wrangler queues send my-queue '{"test": "message"}' +``` + +## Debugging & Troubleshooting + +### Tail Logs (Real-time) + +```bash +# Tail logs from production +wrangler tail + +# Tail specific environment +wrangler tail --env staging + +# Filter by status +wrangler tail --status error + +# Filter by method +wrangler tail --method POST + +# Pretty print +wrangler tail --format pretty +``` + +### Deployment Issues + +**Version conflicts:** +```bash +# Force overwrite +wrangler deploy --force +``` + +**Bundle size issues:** +```bash +# Check bundle size +wrangler deploy --dry-run --outdir=dist + +# Optimize +npm run build -- --minify +``` + +**Authentication issues:** +```bash +# Re-login +wrangler login + +# Use API token +export CLOUDFLARE_API_TOKEN=your-token +``` + +## Best Practices + +### Configuration Management + +1. **Use environments** for staging/production +2. **Store secrets in Wrangler**, not in config files +3. **Use compatibility dates** to lock runtime behavior +4. **Version control** your wrangler.toml + +### Deployment Strategy + +1. **Test locally** with `wrangler dev` +2. **Deploy to staging** first +3. **Use gradual rollouts** for production +4. **Monitor logs** during deployment +5. **Keep previous versions** for quick rollback + +### CI/CD Best Practices + +1. **Separate staging and production** workflows +2. **Use deployment keys** with minimal permissions +3. **Run tests** before deployment +4. **Tag releases** in git +5. **Notify team** on deployments + +### Performance Optimization + +1. **Minimize bundle size** - Tree-shake unused code +2. **Use custom builds** for complex projects +3. **Enable build caching** in CI/CD +4. **Optimize dependencies** - Use smaller packages + +## Advanced Features + +### Custom Domains + +```toml +routes = [ + { pattern = "api.example.com/*", zone_name = "example.com", custom_domain = true } +] +``` + +```bash +# Add custom domain via CLI +wrangler domains add api.example.com +``` + +### Workers for Platforms + +Deploy user-provided Workers on your infrastructure. + +```toml +[[dispatch_namespaces]] +binding = "DISPATCHER" +namespace = "my-namespace" +outbound = { service = "my-worker" } +``` + +### Smart Placement + +Automatically place Workers near data sources. + +```toml +[placement] +mode = "smart" +``` + +## Additional Resources + +- **Wrangler Docs**: https://developers.cloudflare.com/workers/wrangler/ +- **Configuration**: https://developers.cloudflare.com/workers/wrangler/configuration/ +- **Commands**: https://developers.cloudflare.com/workers/wrangler/commands/ +- **CI/CD**: https://developers.cloudflare.com/workers/ci-cd/ +- **GitHub Actions**: https://github.com/cloudflare/wrangler-action