
Cloudflare Workers Runtime Apis
- 226 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Use cloudflare-workers-runtime-apis for development tasks
About
cloudflare-workers-runtime-apis: A skill for development. This provides functionality for development workflows.
- cloudflare-workers-runtime-apis
Cloudflare Workers Runtime Apis by the numbers
- 226 all-time installs (skills.sh)
- +12 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,757 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill cloudflare-workers-runtime-apisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 226 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Use cloudflare-workers-runtime-apis for development tasks
Files
Cloudflare Workers Runtime APIs
Master the Workers runtime APIs: Fetch, Streams, Crypto, Cache, WebSockets, and text encoding.
Quick Reference
| API | Purpose | Common Use |
|---|---|---|
| Fetch | HTTP requests | External APIs, proxying |
| Streams | Data streaming | Large files, real-time |
| Crypto | Cryptography | Hashing, signing, encryption |
| Cache | Response caching | Performance optimization |
| WebSockets | Real-time connections | Chat, live updates |
| Encoding | Text encoding | UTF-8, Base64 |
Quick Start: Fetch API
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Basic fetch
const response = await fetch('https://api.example.com/data');
// With options
const postResponse = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${env.API_KEY}`,
},
body: JSON.stringify({ name: 'John' }),
});
// Clone for multiple reads
const clone = response.clone();
const json = await response.json();
const text = await clone.text();
return Response.json(json);
}
};Critical Rules
1. Always set timeouts for external requests - Workers have a 30s limit, external APIs can hang 2. Clone responses before reading body - Body can only be read once 3. Use streaming for large payloads - Don't buffer entire response in memory 4. Cache external API responses - Reduce latency and API costs 5. Handle Crypto operations in try/catch - Invalid inputs throw errors 6. WebSocket hibernation for cost - Use Durable Objects with hibernation
Top 10 Errors Prevented
| Error | Symptom | Prevention |
|---|---|---|
| Body already read | TypeError: Body has already been consumed | Clone response before reading |
| Fetch timeout | Request hangs, worker times out | Use AbortController with timeout |
| Invalid JSON | SyntaxError: Unexpected token | Check content-type before parsing |
| Stream locked | TypeError: ReadableStream is locked | Don't read stream multiple times |
| Crypto key error | DOMException: Invalid keyData | Validate key format and algorithm |
| Cache miss | Returns undefined instead of response | Check cache before returning |
| WebSocket close | Connection drops unexpectedly | Handle close event, implement reconnect |
| Encoding error | TypeError: Invalid code point | Use TextEncoder/TextDecoder properly |
| CORS blocked | Browser rejects response | Add proper CORS headers |
| Request size | 413 Request Entity Too Large | Stream large uploads |
Fetch API Patterns
With Timeout
async function fetchWithTimeout(url: string, timeout: number = 5000): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
return response;
} finally {
clearTimeout(timeoutId);
}
}With Retry
async function fetchWithRetry(
url: string,
options: RequestInit = {},
retries: number = 3
): Promise<Response> {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// Retry on 5xx errors
if (response.status >= 500 && i < retries - 1) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
continue;
}
return response;
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
}
}
throw new Error('Max retries exceeded');
}Streams API
Transform Stream
function createUppercaseStream(): TransformStream<string, string> {
return new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
}
// Usage
const response = await fetch('https://example.com/text');
const transformed = response.body!
.pipeThrough(new TextDecoderStream())
.pipeThrough(createUppercaseStream())
.pipeThrough(new TextEncoderStream());
return new Response(transformed);Stream Large Response
async function streamLargeFile(url: string): Promise<Response> {
const response = await fetch(url);
// Stream directly without buffering
return new Response(response.body, {
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/octet-stream',
},
});
}Crypto API
Hashing
async function sha256(data: string): Promise<string> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}HMAC Signing
async function signHMAC(key: string, data: string): Promise<string> {
const encoder = new TextEncoder();
const keyData = encoder.encode(key);
const dataBuffer = encoder.encode(data);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, dataBuffer);
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}Cache API
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const cache = caches.default;
const cacheKey = new Request(request.url, { method: 'GET' });
// Check cache
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// Fetch and cache
response = await fetch(request);
response = new Response(response.body, response);
response.headers.set('Cache-Control', 'public, max-age=3600');
// Store in cache (don't await)
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
}
};WebSockets (Durable Objects)
// Durable Object with WebSocket hibernation
export class WebSocketRoom {
state: DurableObjectState;
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected websocket', { status: 426 });
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// Accept with hibernation
this.state.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
// Handle incoming message
const data = typeof message === 'string' ? message : new TextDecoder().decode(message);
// Broadcast to all connected clients
for (const client of this.state.getWebSockets()) {
client.send(data);
}
}
async webSocketClose(ws: WebSocket, code: number, reason: string) {
ws.close(code, reason);
}
}When to Load References
Load specific references based on the task:
- Making HTTP requests? → Load
references/fetch-api.mdfor timeout, retry, proxy patterns - Processing large data? → Load
references/streams-api.mdfor TransformStream, chunking - Encryption/signing? → Load
references/crypto-api.mdfor AES, RSA, JWT verification - Caching responses? → Load
references/cache-api.mdfor Cache API patterns, TTL strategies - Real-time features? → Load
references/websockets.mdfor WebSocket patterns, Durable Objects - Text encoding? → Load
references/encoding-api.mdfor TextEncoder, Base64, Unicode
Templates
| Template | Purpose | Use When |
|---|---|---|
templates/fetch-patterns.ts | HTTP request utilities | Building API clients |
templates/stream-processing.ts | Stream transformation | Processing large files |
templates/crypto-operations.ts | Crypto utilities | Signing, hashing, encryption |
templates/websocket-handler.ts | WebSocket DO | Real-time applications |
Resources
- Runtime APIs: https://developers.cloudflare.com/workers/runtime-apis/
- Fetch API: https://developers.cloudflare.com/workers/runtime-apis/fetch/
- Streams API: https://developers.cloudflare.com/workers/runtime-apis/streams/
- Web Crypto: https://developers.cloudflare.com/workers/runtime-apis/web-crypto/
- Cache API: https://developers.cloudflare.com/workers/runtime-apis/cache/
- WebSockets: https://developers.cloudflare.com/workers/runtime-apis/websockets/
Cache API in Cloudflare Workers
Caching responses at the edge for improved performance.
Cache Types
| Cache | Scope | Use Case |
|---|---|---|
caches.default | Cloudflare's edge cache | CDN caching, shared across workers |
caches.open('name') | Custom named cache | Worker-specific caching |
Basic Usage
Read from Cache
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const cache = caches.default;
const cacheKey = new Request(request.url, { method: 'GET' });
// Check cache first
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
// Fetch and cache
const response = await fetch(request);
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
},
};Custom Cache Key
function createCacheKey(request: Request): Request {
const url = new URL(request.url);
// Include specific query params in cache key
const cacheUrl = new URL(url.origin + url.pathname);
cacheUrl.searchParams.set('version', url.searchParams.get('version') || 'v1');
return new Request(cacheUrl.toString(), {
method: 'GET',
headers: {
// Include headers that affect response
'Accept-Language': request.headers.get('Accept-Language') || 'en',
},
});
}Cache Control
Setting Cache TTL
async function cacheWithTTL(
cache: Cache,
cacheKey: Request,
response: Response,
ttlSeconds: number
): Promise<void> {
const cachedResponse = new Response(response.body, response);
// Set cache headers
cachedResponse.headers.set('Cache-Control', `public, max-age=${ttlSeconds}`);
await cache.put(cacheKey, cachedResponse);
}Cache Headers
// Vary by header (different cache per value)
response.headers.set('Vary', 'Accept-Language, Accept-Encoding');
// Cache for 1 hour
response.headers.set('Cache-Control', 'public, max-age=3600');
// Cache for 1 hour, stale-while-revalidate for 1 day
response.headers.set(
'Cache-Control',
'public, max-age=3600, stale-while-revalidate=86400'
);
// Don't cache
response.headers.set('Cache-Control', 'no-store');
// Private cache (browser only, not CDN)
response.headers.set('Cache-Control', 'private, max-age=3600');Cache Patterns
Cache Aside Pattern
async function getCachedOrFetch<T>(
cacheKey: string,
fetchFn: () => Promise<T>,
ttlSeconds: number = 3600
): Promise<T> {
const cache = caches.default;
const request = new Request(`https://cache/${cacheKey}`);
// Try cache
const cached = await cache.match(request);
if (cached) {
return cached.json();
}
// Fetch fresh data
const data = await fetchFn();
// Store in cache
const response = Response.json(data, {
headers: {
'Cache-Control': `public, max-age=${ttlSeconds}`,
},
});
await cache.put(request, response);
return data;
}
// Usage
const users = await getCachedOrFetch(
'users-list',
() => db.query('SELECT * FROM users'),
300
);Stale-While-Revalidate
async function staleWhileRevalidate(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const cache = caches.default;
const cached = await cache.match(request);
// Return stale and refresh in background
if (cached) {
// Check if stale (past max-age but within stale-while-revalidate)
const age = parseInt(cached.headers.get('Age') || '0');
const maxAge = 3600; // 1 hour
if (age > maxAge) {
// Revalidate in background
ctx.waitUntil(refreshCache(request, cache));
}
return cached;
}
// No cache - fetch and cache
return fetchAndCache(request, cache, ctx);
}
async function refreshCache(request: Request, cache: Cache): Promise<void> {
const fresh = await fetch(request);
if (fresh.ok) {
const response = new Response(fresh.body, fresh);
response.headers.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400');
await cache.put(request, response);
}
}Cache with Fallback
async function cacheWithFallback(
request: Request,
ctx: ExecutionContext
): Promise<Response> {
const cache = caches.default;
try {
// Try origin
const response = await fetch(request);
if (response.ok) {
// Cache successful response
ctx.waitUntil(cache.put(request, response.clone()));
return response;
}
// Origin error - try cache
const cached = await cache.match(request);
if (cached) {
return cached;
}
return response; // Return error response
} catch (error) {
// Network error - try cache
const cached = await cache.match(request);
if (cached) {
return cached;
}
throw error;
}
}Cache Invalidation
Delete Single Entry
async function invalidateCache(cacheKey: string): Promise<boolean> {
const cache = caches.default;
const request = new Request(`https://cache/${cacheKey}`);
return cache.delete(request);
}Purge by Tag (Using Cloudflare API)
async function purgeByTag(
zoneId: string,
tags: string[],
apiToken: string
): Promise<void> {
await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags }),
});
}Versioned Cache Keys
// Include version in cache key to invalidate on deployment
const CACHE_VERSION = 'v1';
function createVersionedCacheKey(path: string): Request {
return new Request(`https://cache/${CACHE_VERSION}/${path}`);
}Named Caches
// Create named cache for specific purpose
const apiCache = await caches.open('api-responses');
const imageCache = await caches.open('image-cache');
// Use like default cache
await apiCache.put(request, response);
const cached = await apiCache.match(request);
await apiCache.delete(request);Cache Headers Explained
| Header | Purpose | Example |
|---|---|---|
Cache-Control | Caching directives | public, max-age=3600 |
Vary | Cache key variations | Vary: Accept-Language |
ETag | Content validation | ETag: "abc123" |
Last-Modified | Time-based validation | Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT |
Age | Time since cached | Age: 600 |
X-Cache | Custom cache status | X-Cache: HIT |
Best Practices
1. Use appropriate TTLs - Balance freshness vs performance 2. Vary by relevant headers - Don't over-vary (cache explosion) 3. Handle cache misses gracefully - Always have fallback 4. Version your cache keys - Easy invalidation on deploy 5. Don't cache personalized content - Unless using Vary properly 6. Use stale-while-revalidate - Better UX for stale data 7. Monitor cache hit rates - Optimize based on data
Web Crypto API in Cloudflare Workers
Cryptographic operations: hashing, signing, encryption, and key management.
Supported Algorithms
| Category | Algorithms |
|---|---|
| Digest | SHA-1, SHA-256, SHA-384, SHA-512 |
| Sign/Verify | RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, HMAC, Ed25519 |
| Encrypt/Decrypt | RSA-OAEP, AES-CTR, AES-CBC, AES-GCM |
| Key Derivation | PBKDF2, HKDF |
| Key Wrapping | AES-KW, RSA-OAEP, AES-GCM |
Hashing
SHA-256
async function sha256(data: string): Promise<string> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
// Usage
const hash = await sha256('Hello, World!');
// => "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"Hash File/Stream
async function hashStream(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const combined = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
const hashBuffer = await crypto.subtle.digest('SHA-256', combined);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}HMAC Signing
Create HMAC Signature
async function signHMAC(key: string, data: string): Promise<string> {
const encoder = new TextEncoder();
const keyData = encoder.encode(key);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
cryptoKey,
encoder.encode(data)
);
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}Verify HMAC Signature
async function verifyHMAC(
key: string,
data: string,
signature: string
): Promise<boolean> {
const encoder = new TextEncoder();
const keyData = encoder.encode(key);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const signatureBytes = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
return crypto.subtle.verify(
'HMAC',
cryptoKey,
signatureBytes,
encoder.encode(data)
);
}AES Encryption
AES-GCM Encryption
interface EncryptedData {
ciphertext: string;
iv: string;
}
async function encryptAES(
key: string,
plaintext: string
): Promise<EncryptedData> {
const encoder = new TextEncoder();
// Derive key from password
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const cryptoKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode('static-salt'), // Use random salt in production
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
// Generate random IV
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
encoder.encode(plaintext)
);
return {
ciphertext: btoa(String.fromCharCode(...new Uint8Array(ciphertext))),
iv: btoa(String.fromCharCode(...iv)),
};
}AES-GCM Decryption
async function decryptAES(
key: string,
encrypted: EncryptedData
): Promise<string> {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const cryptoKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode('static-salt'),
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
const iv = Uint8Array.from(atob(encrypted.iv), (c) => c.charCodeAt(0));
const ciphertext = Uint8Array.from(atob(encrypted.ciphertext), (c) =>
c.charCodeAt(0)
);
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
ciphertext
);
return decoder.decode(plaintext);
}RSA Operations
Generate RSA Key Pair
async function generateRSAKeyPair(): Promise<CryptoKeyPair> {
return crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
true,
['encrypt', 'decrypt']
);
}RSA Encrypt/Decrypt
async function rsaEncrypt(publicKey: CryptoKey, data: string): Promise<string> {
const encrypted = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
new TextEncoder().encode(data)
);
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}
async function rsaDecrypt(
privateKey: CryptoKey,
encrypted: string
): Promise<string> {
const data = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
const decrypted = await crypto.subtle.decrypt(
{ name: 'RSA-OAEP' },
privateKey,
data
);
return new TextDecoder().decode(decrypted);
}JWT Operations
Verify JWT
interface JWTPayload {
sub?: string;
exp?: number;
iat?: number;
[key: string]: unknown;
}
async function verifyJWT(token: string, secret: string): Promise<JWTPayload> {
const [headerB64, payloadB64, signatureB64] = token.split('.');
if (!headerB64 || !payloadB64 || !signatureB64) {
throw new Error('Invalid JWT format');
}
// Verify signature
const signatureValid = await verifyHMAC(
secret,
`${headerB64}.${payloadB64}`,
signatureB64.replace(/-/g, '+').replace(/_/g, '/')
);
if (!signatureValid) {
throw new Error('Invalid JWT signature');
}
// Decode payload
const payload = JSON.parse(
new TextDecoder().decode(
Uint8Array.from(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')), (c) =>
c.charCodeAt(0)
)
)
) as JWTPayload;
// Check expiration
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('JWT expired');
}
return payload;
}Sign JWT
async function signJWT(payload: JWTPayload, secret: string): Promise<string> {
const header = { alg: 'HS256', typ: 'JWT' };
const headerB64 = btoa(JSON.stringify(header))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const payloadB64 = btoa(JSON.stringify(payload))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const signature = await signHMAC(secret, `${headerB64}.${payloadB64}`);
const signatureB64 = signature
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
return `${headerB64}.${payloadB64}.${signatureB64}`;
}Random Values
// Generate random bytes
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
// Generate random UUID
const uuid = crypto.randomUUID();
// Generate random string
function randomString(length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const randomValues = crypto.getRandomValues(new Uint8Array(length));
return Array.from(randomValues)
.map((v) => chars[v % chars.length])
.join('');
}Key Import/Export
Import PEM Key
async function importPublicKeyPEM(pem: string): Promise<CryptoKey> {
// Remove PEM headers and decode base64
const pemContents = pem
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.replace(/\s/g, '');
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
return crypto.subtle.importKey(
'spki',
binaryDer,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
true,
['encrypt']
);
}Best Practices
1. Use random IVs - Never reuse IVs with same key 2. Secure key storage - Use Workers Secrets for keys 3. Appropriate algorithm - Use AES-GCM for symmetric, RSA-OAEP for asymmetric 4. Validate inputs - Check key/data formats before crypto operations 5. Handle errors - Crypto operations throw on invalid input 6. Constant-time comparison - Use crypto.subtle.verify for signatures
Encoding APIs in Cloudflare Workers
Text encoding, Base64, and binary data handling.
Text Encoding
TextEncoder
Encodes strings to UTF-8 bytes:
const encoder = new TextEncoder();
// Encode string to Uint8Array
const bytes = encoder.encode('Hello, World!');
// => Uint8Array([72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33])
// Encode into existing buffer
const buffer = new Uint8Array(20);
const result = encoder.encodeInto('Hello', buffer);
// result: { read: 5, written: 5 }TextDecoder
Decodes bytes to strings:
const decoder = new TextDecoder();
// Decode UTF-8 bytes to string
const text = decoder.decode(new Uint8Array([72, 101, 108, 108, 111]));
// => "Hello"
// Decode with different encoding
const utf16Decoder = new TextDecoder('utf-16');
const latin1Decoder = new TextDecoder('iso-8859-1');
// Streaming decode
const streamDecoder = new TextDecoder('utf-8', { stream: true });
let result = '';
result += streamDecoder.decode(chunk1, { stream: true });
result += streamDecoder.decode(chunk2, { stream: true });
result += streamDecoder.decode(); // Final flushSupported Encodings
| Encoding | Description |
|---|---|
utf-8 | Default, most common |
utf-16le, utf-16be | UTF-16 variants |
iso-8859-1 | Latin-1 |
windows-1252 | Windows Latin |
Base64 Encoding
Standard Base64
// Encode string to Base64
const base64 = btoa('Hello, World!');
// => "SGVsbG8sIFdvcmxkIQ=="
// Decode Base64 to string
const text = atob('SGVsbG8sIFdvcmxkIQ==');
// => "Hello, World!"Binary Data to Base64
// Uint8Array to Base64
function uint8ArrayToBase64(bytes: Uint8Array): string {
return btoa(String.fromCharCode(...bytes));
}
// Base64 to Uint8Array
function base64ToUint8Array(base64: string): Uint8Array {
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
// Usage
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
const encoded = uint8ArrayToBase64(bytes);
const decoded = base64ToUint8Array(encoded);URL-Safe Base64
// Encode to URL-safe Base64
function toBase64Url(data: string | Uint8Array): string {
const base64 = typeof data === 'string'
? btoa(data)
: btoa(String.fromCharCode(...data));
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// Decode from URL-safe Base64
function fromBase64Url(base64url: string): string {
const base64 = base64url
.replace(/-/g, '+')
.replace(/_/g, '/');
// Add padding if needed
const padding = base64.length % 4;
const padded = padding ? base64 + '='.repeat(4 - padding) : base64;
return atob(padded);
}
// To Uint8Array
function base64UrlToUint8Array(base64url: string): Uint8Array {
return Uint8Array.from(fromBase64Url(base64url), (c) => c.charCodeAt(0));
}ArrayBuffer Operations
Creating ArrayBuffers
// From size
const buffer = new ArrayBuffer(16);
// From typed array
const uint8 = new Uint8Array([1, 2, 3, 4]);
const arrayBuffer = uint8.buffer;
// Copy ArrayBuffer
function copyArrayBuffer(source: ArrayBuffer): ArrayBuffer {
const copy = new ArrayBuffer(source.byteLength);
new Uint8Array(copy).set(new Uint8Array(source));
return copy;
}Concatenating Buffers
function concatBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const buffer of buffers) {
result.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
}
return result.buffer;
}Slicing Buffers
function sliceBuffer(buffer: ArrayBuffer, start: number, end?: number): ArrayBuffer {
return buffer.slice(start, end);
}
// Using views (no copy)
function viewBuffer(buffer: ArrayBuffer, start: number, length: number): Uint8Array {
return new Uint8Array(buffer, start, length);
}Typed Arrays
| Type | Size | Range |
|---|---|---|
Uint8Array | 1 byte | 0 - 255 |
Int8Array | 1 byte | -128 - 127 |
Uint16Array | 2 bytes | 0 - 65535 |
Int16Array | 2 bytes | -32768 - 32767 |
Uint32Array | 4 bytes | 0 - 4294967295 |
Int32Array | 4 bytes | -2147483648 - 2147483647 |
Float32Array | 4 bytes | IEEE 754 float |
Float64Array | 8 bytes | IEEE 754 double |
BigUint64Array | 8 bytes | 0 - 2^64-1 |
BigInt64Array | 8 bytes | -2^63 - 2^63-1 |
// Convert between typed arrays
const uint8 = new Uint8Array([1, 2, 3, 4]);
const uint32 = new Uint32Array(uint8.buffer);
// Read specific bytes as different type
const dataView = new DataView(uint8.buffer);
const bigEndian = dataView.getUint32(0, false); // big endian
const littleEndian = dataView.getUint32(0, true); // little endianHex Encoding
// Bytes to hex string
function toHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
// Hex string to bytes
function fromHex(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
// Usage
const bytes = new Uint8Array([255, 128, 64]);
const hex = toHex(bytes); // "ff8040"
const back = fromHex(hex); // Uint8Array([255, 128, 64])Stream Encoding/Decoding
TextDecoderStream
// Transform stream of bytes to text
const response = await fetch('https://example.com/text');
const textStream = response.body!.pipeThrough(new TextDecoderStream());
const reader = textStream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // string chunks
}TextEncoderStream
// Transform stream of text to bytes
function createTextStream(text: string): ReadableStream<Uint8Array> {
return new ReadableStream({
start(controller) {
controller.enqueue(text);
controller.close();
},
}).pipeThrough(new TextEncoderStream());
}JSON Handling
// Safe JSON parse
function safeJsonParse<T>(text: string, fallback: T): T {
try {
return JSON.parse(text) as T;
} catch {
return fallback;
}
}
// JSON from ArrayBuffer
function jsonFromBuffer<T>(buffer: ArrayBuffer): T {
const text = new TextDecoder().decode(buffer);
return JSON.parse(text);
}
// JSON to ArrayBuffer
function jsonToBuffer(data: unknown): ArrayBuffer {
const text = JSON.stringify(data);
return new TextEncoder().encode(text).buffer;
}FormData Encoding
// Create FormData
const formData = new FormData();
formData.append('name', 'John');
formData.append('file', new Blob(['content'], { type: 'text/plain' }), 'file.txt');
// Parse FormData from request
async function parseFormData(request: Request): Promise<Map<string, string | File>> {
const formData = await request.formData();
const result = new Map<string, string | File>();
for (const [key, value] of formData.entries()) {
result.set(key, value);
}
return result;
}Best Practices
1. Use TextEncoder/Decoder - Standard and efficient 2. Prefer URL-safe Base64 - For URLs and cookies 3. Reuse encoders - Create once, use many times 4. Stream large data - Use TextDecoderStream/TextEncoderStream 5. Handle encoding errors - Use try/catch for invalid input 6. Check encoding support - Not all encodings available everywhere
Fetch API in Cloudflare Workers
Complete guide to making HTTP requests from Workers.
Basic Fetch
// GET request
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// POST request
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'John', email: 'john@example.com' }),
});Request Options
interface FetchOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
headers: HeadersInit;
body: BodyInit | null;
redirect: 'follow' | 'manual' | 'error';
signal: AbortSignal;
cf: RequestInitCfProperties; // Cloudflare-specific
}Cloudflare-Specific Options
const response = await fetch(url, {
cf: {
// Cache settings
cacheTtl: 300,
cacheEverything: true,
cacheKey: 'custom-key',
// Polish (image optimization)
polish: 'lossy',
minify: { javascript: true, css: true, html: true },
// Mirage (image lazy loading)
mirage: true,
// Resolve override
resolveOverride: 'example.com',
// Scrape shield
scrapeShield: false,
// Apps
apps: false,
},
});Timeout with AbortController
async function fetchWithTimeout(
url: string,
options: RequestInit = {},
timeout: number = 5000
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeout}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
// Usage
try {
const response = await fetchWithTimeout('https://slow-api.com', {}, 3000);
} catch (error) {
console.error('Fetch failed:', error.message);
}Retry with Exponential Backoff
interface RetryOptions {
retries?: number;
baseDelay?: number;
maxDelay?: number;
retryOn?: (response: Response) => boolean;
}
async function fetchWithRetry(
url: string,
options: RequestInit = {},
retryOptions: RetryOptions = {}
): Promise<Response> {
const {
retries = 3,
baseDelay = 1000,
maxDelay = 10000,
retryOn = (r) => r.status >= 500,
} = retryOptions;
let lastError: Error | null = null;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok || !retryOn(response)) {
return response;
}
// Should retry
if (attempt < retries) {
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
await new Promise((r) => setTimeout(r, delay));
} else {
return response; // Return last response if all retries exhausted
}
} catch (error) {
lastError = error as Error;
if (attempt < retries) {
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
await new Promise((r) => setTimeout(r, delay));
}
}
}
throw lastError || new Error('Max retries exceeded');
}Response Handling
Reading Response Body
// JSON
const json = await response.json();
// Text
const text = await response.text();
// ArrayBuffer
const buffer = await response.arrayBuffer();
// Blob
const blob = await response.blob();
// FormData
const formData = await response.formData();
// Stream
const stream = response.body; // ReadableStreamResponse Cloning
// Body can only be read once - clone if needed multiple times
const response = await fetch(url);
const clone = response.clone();
const json = await response.json();
const text = await clone.text(); // Can read clone separatelyChecking Response
const response = await fetch(url);
// Status checks
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Headers
const contentType = response.headers.get('Content-Type');
const cacheControl = response.headers.get('Cache-Control');
// Redirected?
if (response.redirected) {
console.log('Redirected to:', response.url);
}Proxy Pattern
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// Rewrite to backend
url.hostname = 'api.backend.com';
url.port = '';
url.protocol = 'https:';
// Forward request
const backendRequest = new Request(url.toString(), {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'manual',
});
// Remove headers that shouldn't be forwarded
backendRequest.headers.delete('cf-connecting-ip');
backendRequest.headers.set('X-Forwarded-For', request.headers.get('cf-connecting-ip') || '');
const response = await fetch(backendRequest);
// Modify response if needed
const modifiedResponse = new Response(response.body, response);
modifiedResponse.headers.set('X-Proxy', 'cloudflare-worker');
return modifiedResponse;
},
};Parallel Requests
// Promise.all for independent requests
const [users, posts, comments] = await Promise.all([
fetch('https://api.example.com/users').then((r) => r.json()),
fetch('https://api.example.com/posts').then((r) => r.json()),
fetch('https://api.example.com/comments').then((r) => r.json()),
]);
// Promise.allSettled for fault-tolerant parallel
const results = await Promise.allSettled([
fetch('https://api1.example.com/data'),
fetch('https://api2.example.com/data'),
fetch('https://api3.example.com/data'),
]);
const successfulResponses = results
.filter((r) => r.status === 'fulfilled')
.map((r) => r.value);Streaming Request Body
// Stream large file upload
async function uploadLargeFile(url: string, file: ReadableStream): Promise<Response> {
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
},
body: file,
// @ts-ignore - duplex required for streaming body
duplex: 'half',
});
}Error Handling
async function safeFetch(url: string, options?: RequestInit): Promise<Response> {
try {
const response = await fetch(url, options);
if (!response.ok) {
// Try to get error details from response
let errorMessage = `HTTP ${response.status}`;
try {
const errorBody = await response.text();
errorMessage += `: ${errorBody.substring(0, 200)}`;
} catch {
errorMessage += `: ${response.statusText}`;
}
throw new Error(errorMessage);
}
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request was aborted');
}
if (error.name === 'TypeError') {
throw new Error(`Network error: ${error.message}`);
}
throw error;
}
}CORS Handling
// Add CORS headers to response
function addCorsHeaders(response: Response, origin: string = '*'): Response {
const newResponse = new Response(response.body, response);
newResponse.headers.set('Access-Control-Allow-Origin', origin);
newResponse.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
newResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
newResponse.headers.set('Access-Control-Max-Age', '86400');
return newResponse;
}
// Handle preflight
export default {
async fetch(request: Request): Promise<Response> {
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
});
}
const response = await handleRequest(request);
return addCorsHeaders(response);
},
};Best Practices
1. Always handle errors - Network can fail anytime 2. Set timeouts - Don't let requests hang indefinitely 3. Use retry for transient failures - 5xx errors often recover 4. Clone before reading body - Body stream can only be consumed once 5. Stream large responses - Don't buffer entire response in memory 6. Cache where appropriate - Reduce latency and external API load 7. Add request IDs - For debugging and tracing
Streams API in Cloudflare Workers
Process data efficiently with Web Streams API.
Stream Types
| Type | Purpose | Example |
|---|---|---|
ReadableStream | Read data | Response body, file upload |
WritableStream | Write data | Response construction |
TransformStream | Transform data | Compression, encryption |
ReadableStream
Creating a ReadableStream
// From string
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('Hello, '));
controller.enqueue(new TextEncoder().encode('World!'));
controller.close();
},
});
// From array
function arrayToStream(items: string[]): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
let index = 0;
return new ReadableStream({
pull(controller) {
if (index < items.length) {
controller.enqueue(encoder.encode(items[index]));
index++;
} else {
controller.close();
}
},
});
}Reading a Stream
// Read all at once (buffering)
async function readStreamToString(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
}
return result;
}
// Read chunk by chunk
async function processStreamChunks(stream: ReadableStream<Uint8Array>): Promise<void> {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process chunk
console.log('Received chunk:', value.length, 'bytes');
}
} finally {
reader.releaseLock();
}
}TransformStream
Basic Transform
// Uppercase transform
function createUppercaseTransform(): TransformStream<string, string> {
return new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
});
}
// Line counter transform
function createLineCounter(): TransformStream<string, string> {
let lineNumber = 0;
return new TransformStream({
transform(chunk, controller) {
const lines = chunk.split('\n');
const numbered = lines
.map((line) => {
lineNumber++;
return `${lineNumber}: ${line}`;
})
.join('\n');
controller.enqueue(numbered);
},
});
}JSON Line Transform
// Transform stream of JSON lines
function createJsonLineTransform<T>(): TransformStream<string, T> {
let buffer = '';
return new TransformStream({
transform(chunk, controller) {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (line.trim()) {
try {
controller.enqueue(JSON.parse(line));
} catch {
// Skip invalid JSON
}
}
}
},
flush(controller) {
if (buffer.trim()) {
try {
controller.enqueue(JSON.parse(buffer));
} catch {
// Ignore
}
}
},
});
}Compression Transform
// Gzip compression
function compressStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
return stream.pipeThrough(new CompressionStream('gzip'));
}
// Gzip decompression
function decompressStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
return stream.pipeThrough(new DecompressionStream('gzip'));
}Piping Streams
// Chain transformations
const response = await fetch('https://example.com/large-file.txt');
const processedStream = response.body!
.pipeThrough(new DecompressionStream('gzip'))
.pipeThrough(new TextDecoderStream())
.pipeThrough(createUppercaseTransform())
.pipeThrough(new TextEncoderStream())
.pipeThrough(new CompressionStream('gzip'));
return new Response(processedStream, {
headers: {
'Content-Encoding': 'gzip',
'Content-Type': 'text/plain',
},
});Stream Utilities
Tee (Split) a Stream
// Split stream for multiple consumers
const response = await fetch(url);
const [stream1, stream2] = response.body!.tee();
// Use stream1 for one purpose
const hash = await hashStream(stream1);
// Use stream2 for another
return new Response(stream2);Merge Streams
function mergeStreams(
streams: ReadableStream<Uint8Array>[]
): ReadableStream<Uint8Array> {
const readers = streams.map((s) => s.getReader());
let currentIndex = 0;
return new ReadableStream({
async pull(controller) {
while (currentIndex < readers.length) {
const { done, value } = await readers[currentIndex].read();
if (done) {
currentIndex++;
continue;
}
controller.enqueue(value);
return;
}
controller.close();
},
});
}Stream to Response
// Stream response directly (no buffering)
export default {
async fetch(request: Request): Promise<Response> {
const backendResponse = await fetch('https://api.example.com/large-data');
// Pass through stream without buffering
return new Response(backendResponse.body, {
status: backendResponse.status,
headers: backendResponse.headers,
});
},
};Streaming Patterns
Server-Sent Events (SSE)
function createSSEStream(
generator: AsyncGenerator<string>
): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream({
async pull(controller) {
const { done, value } = await generator.next();
if (done) {
controller.close();
return;
}
// SSE format: data: <message>\n\n
const message = `data: ${value}\n\n`;
controller.enqueue(encoder.encode(message));
},
});
}
// Usage
export default {
async fetch(request: Request): Promise<Response> {
async function* eventGenerator() {
for (let i = 0; i < 10; i++) {
yield JSON.stringify({ count: i, time: Date.now() });
await new Promise((r) => setTimeout(r, 1000));
}
}
return new Response(createSSEStream(eventGenerator()), {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
},
};Chunked Processing
async function processInChunks<T>(
stream: ReadableStream<Uint8Array>,
chunkSize: number,
processor: (chunk: Uint8Array) => Promise<T>
): Promise<T[]> {
const reader = stream.getReader();
const results: T[] = [];
let buffer = new Uint8Array(0);
while (true) {
const { done, value } = await reader.read();
if (value) {
// Append to buffer
const newBuffer = new Uint8Array(buffer.length + value.length);
newBuffer.set(buffer);
newBuffer.set(value, buffer.length);
buffer = newBuffer;
}
// Process complete chunks
while (buffer.length >= chunkSize) {
const chunk = buffer.slice(0, chunkSize);
buffer = buffer.slice(chunkSize);
results.push(await processor(chunk));
}
if (done) {
// Process remaining data
if (buffer.length > 0) {
results.push(await processor(buffer));
}
break;
}
}
return results;
}Stream with Progress
function createProgressStream(
stream: ReadableStream<Uint8Array>,
totalSize: number,
onProgress: (progress: number) => void
): ReadableStream<Uint8Array> {
let bytesRead = 0;
return stream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
bytesRead += chunk.length;
onProgress(bytesRead / totalSize);
controller.enqueue(chunk);
},
})
);
}Error Handling
async function safeStreamRead(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
// Concatenate chunks
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
} catch (error) {
reader.releaseLock();
throw error;
}
}Best Practices
1. Always release reader locks - Use finally blocks or proper cleanup 2. Don't buffer large streams - Process chunks incrementally 3. Use tee() for multiple consumers - Don't read stream twice 4. Handle backpressure - TransformStream handles this automatically 5. Close streams properly - Call controller.close() when done 6. Error propagation - Errors in transforms propagate through pipe chain
WebSockets in Cloudflare Workers
Real-time bidirectional communication with WebSockets.
WebSocket Architecture
Workers support WebSockets in two modes: 1. Proxy Mode - Forward WebSocket connections to origin 2. Durable Objects - Handle WebSocket connections directly with hibernation
Proxy Mode
Simple WebSocket Proxy
export default {
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected websocket', { status: 426 });
}
// Proxy to backend WebSocket server
const backendUrl = new URL(request.url);
backendUrl.hostname = 'ws-backend.example.com';
return fetch(backendUrl.toString(), {
headers: request.headers,
});
},
};Durable Objects WebSockets
Basic WebSocket Handler
// wrangler.jsonc
// {
// "durable_objects": {
// "bindings": [{ "name": "ROOMS", "class_name": "ChatRoom" }]
// }
// }
export class ChatRoom {
state: DurableObjectState;
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected websocket', { status: 426 });
}
// Create WebSocket pair
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// Accept WebSocket with hibernation
this.state.acceptWebSocket(server);
// Return client end to caller
return new Response(null, {
status: 101,
webSocket: client,
});
}
// Called when WebSocket message received
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const data = typeof message === 'string' ? message : new TextDecoder().decode(message);
// Broadcast to all connected clients
const sockets = this.state.getWebSockets();
for (const socket of sockets) {
socket.send(data);
}
}
// Called when WebSocket closes
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
): Promise<void> {
ws.close(code, reason);
}
// Called on WebSocket error
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
console.error('WebSocket error:', error);
ws.close(1011, 'Internal error');
}
}Worker Entry Point
interface Env {
ROOMS: DurableObjectNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const roomName = url.pathname.slice(1) || 'default';
// Get or create room
const roomId = env.ROOMS.idFromName(roomName);
const room = env.ROOMS.get(roomId);
return room.fetch(request);
},
};
export { ChatRoom };WebSocket with Tags
export class TaggedChatRoom {
state: DurableObjectState;
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const userId = url.searchParams.get('userId') || 'anonymous';
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// Accept with tags for filtering
this.state.acceptWebSocket(server, [userId, 'all']);
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const data = JSON.parse(message as string);
if (data.type === 'broadcast') {
// Send to all
const sockets = this.state.getWebSockets('all');
for (const socket of sockets) {
socket.send(JSON.stringify(data));
}
} else if (data.type === 'direct' && data.targetUserId) {
// Send to specific user
const sockets = this.state.getWebSockets(data.targetUserId);
for (const socket of sockets) {
socket.send(JSON.stringify(data));
}
}
}
}WebSocket Hibernation
Hibernation allows WebSocket connections to persist without keeping the Durable Object in memory.
export class HibernatingRoom {
state: DurableObjectState;
storage: DurableObjectStorage;
constructor(state: DurableObjectState) {
this.state = state;
this.storage = state.storage;
}
async fetch(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// Hibernation-aware accept
this.state.acceptWebSocket(server);
// Persist user info
const url = new URL(request.url);
const userId = url.searchParams.get('userId');
if (userId) {
// Attach metadata to WebSocket (survives hibernation)
server.serializeAttachment({ userId, joinedAt: Date.now() });
}
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
// Get metadata
const attachment = ws.deserializeAttachment() as { userId: string; joinedAt: number };
const userId = attachment?.userId || 'anonymous';
const data = {
from: userId,
message: typeof message === 'string' ? message : new TextDecoder().decode(message),
timestamp: Date.now(),
};
// Store message
await this.storage.put(`message:${Date.now()}`, data);
// Broadcast
for (const socket of this.state.getWebSockets()) {
socket.send(JSON.stringify(data));
}
}
async webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void> {
const attachment = ws.deserializeAttachment() as { userId: string };
// Notify others
for (const socket of this.state.getWebSockets()) {
if (socket !== ws) {
socket.send(JSON.stringify({
type: 'user_left',
userId: attachment?.userId,
}));
}
}
ws.close(code, reason);
}
}Client-Side Connection
// Browser client
function connectWebSocket(roomName: string, userId: string): WebSocket {
const ws = new WebSocket(
`wss://my-worker.example.com/${roomName}?userId=${userId}`
);
ws.onopen = () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'join', userId }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
ws.onclose = (event) => {
console.log('Disconnected:', event.code, event.reason);
// Implement reconnection logic
setTimeout(() => connectWebSocket(roomName, userId), 1000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
return ws;
}Message Protocol Design
// Define message types
type ServerMessage =
| { type: 'chat'; from: string; message: string; timestamp: number }
| { type: 'user_joined'; userId: string }
| { type: 'user_left'; userId: string }
| { type: 'error'; message: string };
type ClientMessage =
| { type: 'chat'; message: string }
| { type: 'typing' }
| { type: 'ping' };
async function handleMessage(
ws: WebSocket,
message: ClientMessage,
userId: string
): Promise<void> {
switch (message.type) {
case 'chat':
broadcast({ type: 'chat', from: userId, message: message.message, timestamp: Date.now() });
break;
case 'typing':
broadcastExcept(ws, { type: 'typing', userId });
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
}
}Connection Management
export class ManagedRoom {
state: DurableObjectState;
connections: Map<WebSocket, { userId: string; lastSeen: number }> = new Map();
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
// Update last seen
const conn = this.connections.get(ws);
if (conn) {
conn.lastSeen = Date.now();
}
// Handle message...
}
// Clean up stale connections (call via alarm)
async cleanup(): Promise<void> {
const now = Date.now();
const timeout = 60000; // 1 minute
for (const socket of this.state.getWebSockets()) {
const attachment = socket.deserializeAttachment() as { lastSeen?: number };
if (attachment?.lastSeen && now - attachment.lastSeen > timeout) {
socket.close(1000, 'Idle timeout');
}
}
}
}Best Practices
1. Use hibernation - Reduces costs for idle connections 2. Implement heartbeat/ping - Detect dead connections 3. Handle reconnection - Client should auto-reconnect 4. Validate messages - Don't trust client input 5. Use tags for targeting - Efficient broadcast to subsets 6. Store minimal state - Durable Object storage for persistence 7. Rate limit messages - Prevent abuse
/**
* Cryptographic Operations for Cloudflare Workers
*
* Features:
* - Hashing (SHA-256, SHA-512)
* - HMAC signing/verification
* - AES encryption/decryption
* - JWT handling
* - Random value generation
*
* Usage: Copy needed functions to src/lib/crypto.ts
*/
// ============================================
// HASHING
// ============================================
/**
* Compute SHA-256 hash of a string
*/
export async function sha256(data: string): Promise<string> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
return bufferToHex(hashBuffer);
}
/**
* Compute SHA-512 hash of a string
*/
export async function sha512(data: string): Promise<string> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-512', dataBuffer);
return bufferToHex(hashBuffer);
}
/**
* Hash a file/stream
*/
export async function hashStream(
stream: ReadableStream<Uint8Array>,
algorithm: 'SHA-256' | 'SHA-512' = 'SHA-256'
): Promise<string> {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const combined = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
const hashBuffer = await crypto.subtle.digest(algorithm, combined);
return bufferToHex(hashBuffer);
}
// ============================================
// HMAC
// ============================================
/**
* Create HMAC-SHA256 signature
*/
export async function signHMAC(key: string, data: string): Promise<string> {
const encoder = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
cryptoKey,
encoder.encode(data)
);
return bufferToBase64(signature);
}
/**
* Verify HMAC-SHA256 signature
*/
export async function verifyHMAC(
key: string,
data: string,
signature: string
): Promise<boolean> {
const encoder = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const signatureBytes = base64ToBuffer(signature);
return crypto.subtle.verify(
'HMAC',
cryptoKey,
signatureBytes,
encoder.encode(data)
);
}
// ============================================
// AES ENCRYPTION
// ============================================
export interface EncryptedData {
ciphertext: string;
iv: string;
salt: string;
}
/**
* Encrypt data with AES-GCM
*/
export async function encryptAES(
password: string,
plaintext: string
): Promise<EncryptedData> {
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
// Derive key from password
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(plaintext)
);
return {
ciphertext: bufferToBase64(ciphertext),
iv: bufferToBase64(iv),
salt: bufferToBase64(salt),
};
}
/**
* Decrypt AES-GCM encrypted data
*/
export async function decryptAES(
password: string,
encrypted: EncryptedData
): Promise<string> {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const salt = base64ToBuffer(encrypted.salt);
const iv = base64ToBuffer(encrypted.iv);
const ciphertext = base64ToBuffer(encrypted.ciphertext);
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
ciphertext
);
return decoder.decode(plaintext);
}
// ============================================
// JWT
// ============================================
export interface JWTPayload {
sub?: string;
exp?: number;
iat?: number;
iss?: string;
aud?: string | string[];
[key: string]: unknown;
}
/**
* Create a JWT token
*/
export async function createJWT(
payload: JWTPayload,
secret: string,
expiresIn: number = 3600
): Promise<string> {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const fullPayload = {
...payload,
iat: now,
exp: now + expiresIn,
};
const headerB64 = base64UrlEncode(JSON.stringify(header));
const payloadB64 = base64UrlEncode(JSON.stringify(fullPayload));
const message = `${headerB64}.${payloadB64}`;
const signature = await signHMAC(secret, message);
const signatureB64 = base64ToBase64Url(signature);
return `${message}.${signatureB64}`;
}
/**
* Verify and decode a JWT token
*/
export async function verifyJWT(
token: string,
secret: string
): Promise<JWTPayload> {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format');
}
const [headerB64, payloadB64, signatureB64] = parts;
const message = `${headerB64}.${payloadB64}`;
const signature = base64UrlToBase64(signatureB64);
const isValid = await verifyHMAC(secret, message, signature);
if (!isValid) {
throw new Error('Invalid JWT signature');
}
const payload = JSON.parse(base64UrlDecode(payloadB64)) as JWTPayload;
// Check expiration
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < now) {
throw new Error('JWT expired');
}
return payload;
}
// ============================================
// RANDOM VALUES
// ============================================
/**
* Generate random bytes
*/
export function randomBytes(length: number): Uint8Array {
return crypto.getRandomValues(new Uint8Array(length));
}
/**
* Generate random hex string
*/
export function randomHex(length: number): string {
const bytes = randomBytes(Math.ceil(length / 2));
return bufferToHex(bytes).slice(0, length);
}
/**
* Generate random alphanumeric string
*/
export function randomString(length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const bytes = randomBytes(length);
return Array.from(bytes)
.map((b) => chars[b % chars.length])
.join('');
}
/**
* Generate random UUID
*/
export function randomUUID(): string {
return crypto.randomUUID();
}
// ============================================
// HELPER FUNCTIONS
// ============================================
function bufferToHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function bufferToBase64(buffer: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
function base64ToBuffer(base64: string): Uint8Array {
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
function base64UrlEncode(data: string): string {
return btoa(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function base64UrlDecode(data: string): string {
const padded = data + '='.repeat((4 - (data.length % 4)) % 4);
return atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
}
function base64ToBase64Url(base64: string): string {
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function base64UrlToBase64(base64url: string): string {
const padded = base64url + '='.repeat((4 - (base64url.length % 4)) % 4);
return padded.replace(/-/g, '+').replace(/_/g, '/');
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
import { sha256, encryptAES, decryptAES, createJWT, verifyJWT } from './lib/crypto';
// Hash a password
const hashedPassword = await sha256(userPassword + salt);
// Encrypt sensitive data
const encrypted = await encryptAES('master-key', JSON.stringify(sensitiveData));
const decrypted = await decryptAES('master-key', encrypted);
// JWT auth
const token = await createJWT({ sub: userId, role: 'admin' }, env.JWT_SECRET, 3600);
const payload = await verifyJWT(token, env.JWT_SECRET);
*/
/**
* Fetch API Utility Patterns for Cloudflare Workers
*
* Features:
* - Timeout with AbortController
* - Retry with exponential backoff
* - Response cloning and handling
* - Parallel requests
* - CORS handling
*
* Usage: Copy needed functions to src/lib/fetch.ts
*/
// ============================================
// FETCH WITH TIMEOUT
// ============================================
export interface TimeoutOptions {
timeout?: number; // ms
}
export async function fetchWithTimeout(
url: string,
options: RequestInit & TimeoutOptions = {}
): Promise<Response> {
const { timeout = 5000, ...fetchOptions } = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
});
return response;
} catch (error) {
if ((error as Error).name === 'AbortError') {
throw new Error(`Request timed out after ${timeout}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
// ============================================
// FETCH WITH RETRY
// ============================================
export interface RetryOptions {
retries?: number;
baseDelay?: number;
maxDelay?: number;
retryOn?: (response: Response) => boolean;
onRetry?: (attempt: number, error?: Error, response?: Response) => void;
}
export async function fetchWithRetry(
url: string,
options: RequestInit & RetryOptions = {}
): Promise<Response> {
const {
retries = 3,
baseDelay = 1000,
maxDelay = 10000,
retryOn = (r) => r.status >= 500,
onRetry,
...fetchOptions
} = options;
let lastError: Error | null = null;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetch(url, fetchOptions);
if (response.ok || !retryOn(response)) {
return response;
}
if (attempt < retries) {
onRetry?.(attempt + 1, undefined, response);
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
await new Promise((r) => setTimeout(r, delay));
} else {
return response;
}
} catch (error) {
lastError = error as Error;
if (attempt < retries) {
onRetry?.(attempt + 1, lastError);
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
await new Promise((r) => setTimeout(r, delay));
}
}
}
throw lastError || new Error('Max retries exceeded');
}
// ============================================
// COMBINED: TIMEOUT + RETRY
// ============================================
export async function robustFetch(
url: string,
options: RequestInit & TimeoutOptions & RetryOptions = {}
): Promise<Response> {
const { timeout = 5000, retries = 3, ...restOptions } = options;
return fetchWithRetry(url, {
...restOptions,
retries,
// Wrap each attempt with timeout
});
}
// ============================================
// PARALLEL FETCH
// ============================================
export interface ParallelFetchResult<T> {
success: T[];
failed: Array<{ url: string; error: Error }>;
}
export async function fetchParallel<T>(
urls: string[],
options: RequestInit = {},
transform: (response: Response) => Promise<T> = (r) => r.json() as Promise<T>
): Promise<ParallelFetchResult<T>> {
const results = await Promise.allSettled(
urls.map(async (url) => {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return transform(response);
})
);
const success: T[] = [];
const failed: Array<{ url: string; error: Error }> = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
success.push(result.value);
} else {
failed.push({ url: urls[index], error: result.reason });
}
});
return { success, failed };
}
// ============================================
// RESPONSE HELPERS
// ============================================
export async function safeJson<T>(response: Response, fallback: T): Promise<T> {
try {
return (await response.json()) as T;
} catch {
return fallback;
}
}
export async function checkResponse(response: Response): Promise<Response> {
if (!response.ok) {
let errorBody = '';
try {
errorBody = await response.text();
} catch {
errorBody = response.statusText;
}
throw new Error(`HTTP ${response.status}: ${errorBody.substring(0, 200)}`);
}
return response;
}
// ============================================
// API CLIENT BUILDER
// ============================================
export interface ApiClientOptions {
baseUrl: string;
headers?: Record<string, string>;
timeout?: number;
retries?: number;
}
export function createApiClient(options: ApiClientOptions) {
const { baseUrl, headers = {}, timeout = 5000, retries = 2 } = options;
async function request<T>(
path: string,
init: RequestInit = {}
): Promise<T> {
const url = `${baseUrl}${path}`;
const response = await fetchWithRetry(url, {
...init,
headers: { ...headers, ...init.headers },
retries,
});
await checkResponse(response);
return response.json() as Promise<T>;
}
return {
get: <T>(path: string, params?: Record<string, string>) => {
const query = params ? `?${new URLSearchParams(params)}` : '';
return request<T>(`${path}${query}`);
},
post: <T>(path: string, body: unknown) =>
request<T>(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}),
put: <T>(path: string, body: unknown) =>
request<T>(path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}),
delete: <T>(path: string) =>
request<T>(path, { method: 'DELETE' }),
};
}
// ============================================
// CORS HANDLING
// ============================================
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
export function addCorsHeaders(response: Response): Response {
const newResponse = new Response(response.body, response);
Object.entries(CORS_HEADERS).forEach(([key, value]) => {
newResponse.headers.set(key, value);
});
return newResponse;
}
export function handleCorsPreFlight(request: Request): Response | null {
if (request.method === 'OPTIONS') {
return new Response(null, { headers: CORS_HEADERS });
}
return null;
}
// ============================================
// PROXY HELPER
// ============================================
export interface ProxyOptions {
targetHost: string;
rewriteHost?: boolean;
addHeaders?: Record<string, string>;
removeHeaders?: string[];
}
export async function proxyRequest(
request: Request,
options: ProxyOptions
): Promise<Response> {
const url = new URL(request.url);
url.hostname = options.targetHost;
url.protocol = 'https:';
const headers = new Headers(request.headers);
// Remove specified headers
options.removeHeaders?.forEach((h) => headers.delete(h));
// Add specified headers
Object.entries(options.addHeaders || {}).forEach(([k, v]) => {
headers.set(k, v);
});
// Optionally rewrite Host header
if (options.rewriteHost) {
headers.set('Host', options.targetHost);
}
const proxyRequest = new Request(url.toString(), {
method: request.method,
headers,
body: request.body,
redirect: 'manual',
});
return fetch(proxyRequest);
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
import { fetchWithRetry, createApiClient, addCorsHeaders } from './lib/fetch';
// Simple retry fetch
const response = await fetchWithRetry('https://api.example.com/data', {
retries: 3,
retryOn: (r) => r.status >= 500 || r.status === 429,
});
// API client
const api = createApiClient({
baseUrl: 'https://api.example.com',
headers: { Authorization: 'Bearer token' },
});
const users = await api.get<User[]>('/users');
const newUser = await api.post<User>('/users', { name: 'John' });
// CORS wrapper
export default {
async fetch(request: Request): Promise<Response> {
const cors = handleCorsPreFlight(request);
if (cors) return cors;
const response = await handleRequest(request);
return addCorsHeaders(response);
}
};
*/
/**
* Stream Processing Utilities for Cloudflare Workers
*
* Features:
* - ReadableStream creation and consumption
* - TransformStream patterns
* - Stream utilities (tee, merge, concat)
* - SSE streaming
*
* Usage: Copy needed functions to src/lib/streams.ts
*/
// ============================================
// STREAM CREATION
// ============================================
/**
* Create a ReadableStream from an array of items
*/
export function arrayToStream<T>(items: T[]): ReadableStream<T> {
let index = 0;
return new ReadableStream({
pull(controller) {
if (index < items.length) {
controller.enqueue(items[index++]);
} else {
controller.close();
}
},
});
}
/**
* Create a ReadableStream from an async generator
*/
export function generatorToStream<T>(
generator: AsyncGenerator<T>
): ReadableStream<T> {
return new ReadableStream({
async pull(controller) {
const { done, value } = await generator.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
});
}
/**
* Create a text stream from a string
*/
export function stringToStream(text: string): ReadableStream<Uint8Array> {
return new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(text));
controller.close();
},
});
}
// ============================================
// STREAM CONSUMPTION
// ============================================
/**
* Read entire stream to string
*/
export async function streamToString(
stream: ReadableStream<Uint8Array>
): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
}
return result + decoder.decode();
}
/**
* Read entire stream to ArrayBuffer
*/
export async function streamToBuffer(
stream: ReadableStream<Uint8Array>
): Promise<ArrayBuffer> {
const chunks: Uint8Array[] = [];
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result.buffer;
}
/**
* Collect stream to array
*/
export async function streamToArray<T>(stream: ReadableStream<T>): Promise<T[]> {
const reader = stream.getReader();
const items: T[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
items.push(value);
}
return items;
}
// ============================================
// TRANSFORM STREAMS
// ============================================
/**
* Create a mapping transform stream
*/
export function mapStream<T, U>(fn: (item: T) => U): TransformStream<T, U> {
return new TransformStream({
transform(chunk, controller) {
controller.enqueue(fn(chunk));
},
});
}
/**
* Create a filtering transform stream
*/
export function filterStream<T>(
predicate: (item: T) => boolean
): TransformStream<T, T> {
return new TransformStream({
transform(chunk, controller) {
if (predicate(chunk)) {
controller.enqueue(chunk);
}
},
});
}
/**
* JSON Lines parser transform
*/
export function jsonLinesTransform<T>(): TransformStream<string, T> {
let buffer = '';
return new TransformStream({
transform(chunk, controller) {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
try {
controller.enqueue(JSON.parse(line));
} catch {
// Skip invalid JSON
}
}
}
},
flush(controller) {
if (buffer.trim()) {
try {
controller.enqueue(JSON.parse(buffer));
} catch {
// Ignore
}
}
},
});
}
/**
* Chunking transform - split stream into fixed-size chunks
*/
export function chunkTransform(
chunkSize: number
): TransformStream<Uint8Array, Uint8Array> {
let buffer = new Uint8Array(0);
return new TransformStream({
transform(chunk, controller) {
// Combine with buffer
const combined = new Uint8Array(buffer.length + chunk.length);
combined.set(buffer);
combined.set(chunk, buffer.length);
buffer = combined;
// Emit complete chunks
while (buffer.length >= chunkSize) {
controller.enqueue(buffer.slice(0, chunkSize));
buffer = buffer.slice(chunkSize);
}
},
flush(controller) {
if (buffer.length > 0) {
controller.enqueue(buffer);
}
},
});
}
/**
* Rate limiting transform
*/
export function rateLimitTransform<T>(
itemsPerSecond: number
): TransformStream<T, T> {
const delayMs = 1000 / itemsPerSecond;
return new TransformStream({
async transform(chunk, controller) {
controller.enqueue(chunk);
await new Promise((r) => setTimeout(r, delayMs));
},
});
}
// ============================================
// STREAM UTILITIES
// ============================================
/**
* Merge multiple streams into one
*/
export function mergeStreams<T>(
streams: ReadableStream<T>[]
): ReadableStream<T> {
const readers = streams.map((s) => s.getReader());
let currentIndex = 0;
return new ReadableStream({
async pull(controller) {
while (currentIndex < readers.length) {
const { done, value } = await readers[currentIndex].read();
if (done) {
currentIndex++;
continue;
}
controller.enqueue(value);
return;
}
controller.close();
},
});
}
/**
* Interleave multiple streams (round-robin)
*/
export function interleaveStreams<T>(
streams: ReadableStream<T>[]
): ReadableStream<T> {
const readers = streams.map((s) => s.getReader());
const done = new Set<number>();
let index = 0;
return new ReadableStream({
async pull(controller) {
while (done.size < readers.length) {
if (!done.has(index)) {
const { done: isDone, value } = await readers[index].read();
if (isDone) {
done.add(index);
} else {
controller.enqueue(value);
index = (index + 1) % readers.length;
return;
}
}
index = (index + 1) % readers.length;
}
controller.close();
},
});
}
// ============================================
// SSE (SERVER-SENT EVENTS)
// ============================================
export interface SSEEvent {
event?: string;
data: string;
id?: string;
retry?: number;
}
/**
* Format SSE event
*/
export function formatSSE(event: SSEEvent): string {
let result = '';
if (event.event) result += `event: ${event.event}\n`;
if (event.id) result += `id: ${event.id}\n`;
if (event.retry) result += `retry: ${event.retry}\n`;
result += `data: ${event.data}\n\n`;
return result;
}
/**
* Create SSE stream from async generator
*/
export function createSSEStream(
generator: AsyncGenerator<SSEEvent>
): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
return new ReadableStream({
async pull(controller) {
const { done, value } = await generator.next();
if (done) {
controller.close();
return;
}
controller.enqueue(encoder.encode(formatSSE(value)));
},
});
}
/**
* Create SSE Response
*/
export function createSSEResponse(
stream: ReadableStream<Uint8Array>
): Response {
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
import {
streamToString,
jsonLinesTransform,
createSSEStream,
createSSEResponse,
} from './lib/streams';
// Process JSON lines stream
const response = await fetch('https://api.example.com/stream');
const jsonStream = response.body!
.pipeThrough(new TextDecoderStream())
.pipeThrough(jsonLinesTransform<MyType>());
const items = await streamToArray(jsonStream);
// SSE streaming
export default {
async fetch(request: Request): Promise<Response> {
async function* generateEvents(): AsyncGenerator<SSEEvent> {
for (let i = 0; i < 10; i++) {
yield {
event: 'message',
data: JSON.stringify({ count: i, time: Date.now() }),
id: String(i),
};
await new Promise((r) => setTimeout(r, 1000));
}
}
return createSSEResponse(createSSEStream(generateEvents()));
},
};
*/
/**
* WebSocket Handler with Durable Objects
*
* Features:
* - WebSocket connections with hibernation
* - Room-based messaging
* - User presence tracking
* - Message persistence
* - Rate limiting
*
* Usage:
* 1. Copy to src/room.ts (Durable Object)
* 2. Configure wrangler.jsonc durable_objects binding
* 3. Create entry worker that routes to rooms
*/
// ============================================
// TYPES
// ============================================
interface UserInfo {
id: string;
name: string;
joinedAt: number;
lastSeen: number;
}
interface ChatMessage {
type: 'chat';
from: UserInfo;
content: string;
timestamp: number;
}
interface SystemMessage {
type: 'system';
event: 'user_joined' | 'user_left' | 'error';
user?: UserInfo;
message?: string;
timestamp: number;
}
interface PresenceMessage {
type: 'presence';
users: UserInfo[];
timestamp: number;
}
type ServerMessage = ChatMessage | SystemMessage | PresenceMessage;
interface ClientMessage {
type: 'chat' | 'typing' | 'ping';
content?: string;
}
interface Env {
ROOMS: DurableObjectNamespace;
}
// ============================================
// DURABLE OBJECT: CHAT ROOM
// ============================================
export class ChatRoom {
private state: DurableObjectState;
private storage: DurableObjectStorage;
private messageHistory: ChatMessage[] = [];
private rateLimits: Map<string, number[]> = new Map();
constructor(state: DurableObjectState) {
this.state = state;
this.storage = state.storage;
// Load message history on wake
state.blockConcurrencyWhile(async () => {
const stored = await this.storage.get<ChatMessage[]>('messages');
if (stored) {
this.messageHistory = stored.slice(-100); // Keep last 100
}
});
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const upgradeHeader = request.headers.get('Upgrade');
// Handle non-WebSocket requests
if (upgradeHeader !== 'websocket') {
// API endpoints
if (url.pathname === '/history') {
return Response.json(this.messageHistory);
}
if (url.pathname === '/users') {
return Response.json(this.getUsers());
}
return new Response('Expected websocket upgrade', { status: 426 });
}
// Get user info from query params
const userId = url.searchParams.get('userId') || crypto.randomUUID();
const userName = url.searchParams.get('name') || 'Anonymous';
// Create WebSocket pair
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// Attach user info
const userInfo: UserInfo = {
id: userId,
name: userName,
joinedAt: Date.now(),
lastSeen: Date.now(),
};
// Accept with hibernation
this.state.acceptWebSocket(server, [userId]);
server.serializeAttachment(userInfo);
// Send welcome message with history
server.send(
JSON.stringify({
type: 'welcome',
userId,
history: this.messageHistory.slice(-50),
users: this.getUsers(),
})
);
// Notify others
this.broadcast({
type: 'system',
event: 'user_joined',
user: userInfo,
timestamp: Date.now(),
}, server);
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const userInfo = ws.deserializeAttachment() as UserInfo;
userInfo.lastSeen = Date.now();
ws.serializeAttachment(userInfo);
// Rate limiting
if (!this.checkRateLimit(userInfo.id)) {
ws.send(JSON.stringify({
type: 'system',
event: 'error',
message: 'Rate limit exceeded. Please slow down.',
timestamp: Date.now(),
}));
return;
}
try {
const data = JSON.parse(
typeof message === 'string' ? message : new TextDecoder().decode(message)
) as ClientMessage;
switch (data.type) {
case 'chat':
await this.handleChat(ws, userInfo, data.content || '');
break;
case 'typing':
this.handleTyping(ws, userInfo);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
break;
}
} catch {
ws.send(JSON.stringify({
type: 'system',
event: 'error',
message: 'Invalid message format',
timestamp: Date.now(),
}));
}
}
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
): Promise<void> {
const userInfo = ws.deserializeAttachment() as UserInfo;
// Notify others
this.broadcast({
type: 'system',
event: 'user_left',
user: userInfo,
timestamp: Date.now(),
});
ws.close(code, reason);
}
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
console.error('WebSocket error:', error);
ws.close(1011, 'Internal error');
}
// ============================================
// MESSAGE HANDLING
// ============================================
private async handleChat(
ws: WebSocket,
user: UserInfo,
content: string
): Promise<void> {
// Validate
const sanitized = content.trim().slice(0, 1000);
if (!sanitized) return;
const message: ChatMessage = {
type: 'chat',
from: user,
content: sanitized,
timestamp: Date.now(),
};
// Store
this.messageHistory.push(message);
if (this.messageHistory.length > 100) {
this.messageHistory = this.messageHistory.slice(-100);
}
await this.storage.put('messages', this.messageHistory);
// Broadcast to all including sender
this.broadcast(message);
}
private handleTyping(ws: WebSocket, user: UserInfo): void {
// Broadcast typing indicator to others
this.broadcast(
{
type: 'typing',
userId: user.id,
userName: user.name,
timestamp: Date.now(),
} as unknown as ServerMessage,
ws
);
}
// ============================================
// UTILITIES
// ============================================
private broadcast(message: ServerMessage, exclude?: WebSocket): void {
const json = JSON.stringify(message);
for (const socket of this.state.getWebSockets()) {
if (socket !== exclude) {
socket.send(json);
}
}
}
private getUsers(): UserInfo[] {
return this.state.getWebSockets().map((ws) => {
return ws.deserializeAttachment() as UserInfo;
});
}
private checkRateLimit(userId: string): boolean {
const now = Date.now();
const window = 10000; // 10 seconds
const maxMessages = 20; // 20 messages per window
let timestamps = this.rateLimits.get(userId) || [];
timestamps = timestamps.filter((t) => now - t < window);
if (timestamps.length >= maxMessages) {
return false;
}
timestamps.push(now);
this.rateLimits.set(userId, timestamps);
return true;
}
}
// ============================================
// WORKER ENTRY POINT
// ============================================
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Parse room name from path: /room/<name>
const match = url.pathname.match(/^\/room\/([^/]+)/);
if (!match) {
return new Response('Room not found. Use /room/<name>', { status: 404 });
}
const roomName = match[1];
// Get or create room
const roomId = env.ROOMS.idFromName(roomName);
const room = env.ROOMS.get(roomId);
// Forward request to room
return room.fetch(request);
},
};
// ============================================
// WRANGLER CONFIGURATION
// ============================================
/*
// wrangler.jsonc
{
"name": "chat-worker",
"main": "src/index.ts",
"compatibility_date": "2024-01-01",
"durable_objects": {
"bindings": [
{
"name": "ROOMS",
"class_name": "ChatRoom"
}
]
},
"migrations": [
{
"tag": "v1",
"new_classes": ["ChatRoom"]
}
]
}
*/
// ============================================
// CLIENT USAGE
// ============================================
/*
// Browser client
const ws = new WebSocket(
'wss://my-chat.workers.dev/room/general?userId=123&name=John'
);
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message);
};
ws.onopen = () => {
// Send a chat message
ws.send(JSON.stringify({ type: 'chat', content: 'Hello everyone!' }));
};
ws.onclose = () => {
// Reconnect logic
setTimeout(() => reconnect(), 1000);
};
*/