This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / bobbin / worker / src / index.ts
3.5 kB 113 lines
1import { Container } from "@cloudflare/containers"; 2 3export interface Env { 4 BOBBIN: DurableObjectNamespace<BobbinContainer>; 5 RATE_LIMITER: RateLimit; 6 BOBBIN_HYDRANT_URL: string; 7 BOBBIN_SLINGSHOT_URL: string; 8 BOBBIN_LOG: string; 9} 10 11export class BobbinContainer extends Container<Env> { 12 defaultPort = 8090; 13 enableInternet = true; 14 // Bobbin maintains an in-memory index rebuilt from Hydrant replay on every 15 // restart, so we want to avoid sleeping where possible. Override 16 // onActivityExpired() to keep the container alive indefinitely. 17 sleepAfter = "24h"; 18 19 constructor(ctx: DurableObjectState<{}>, env: Env) { 20 super(ctx, env, { 21 envVars: { 22 BOBBIN_HYDRANT_URL: env.BOBBIN_HYDRANT_URL, 23 BOBBIN_SLINGSHOT_URL: env.BOBBIN_SLINGSHOT_URL, 24 BOBBIN_LOG: env.BOBBIN_LOG, 25 BOBBIN_LOG_FORMAT: "json", 26 }, 27 }); 28 } 29 30 async onActivityExpired(): Promise<void> { 31 // Keep the container running; bobbin's in-memory index is expensive to 32 // rebuild. Renew the timeout instead of stopping so we're pinged again later. 33 this.renewActivityTimeout(); 34 } 35 36 onError(error: Error) { 37 console.error("bobbin container error:", error); 38 } 39} 40 41const INDEX = `This is bobbin, Tangled's stateless XRPC API service: https://tangled.org/tangled.org/core/tree/master/bobbin`; 42 43const CORS_HEADERS = { 44 "Access-Control-Allow-Origin": "*", 45 "Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS", 46 "Access-Control-Allow-Headers": "Content-Type, Authorization, atproto-proxy", 47 "Access-Control-Max-Age": "86400", 48}; 49 50function withCors(response: Response): Response { 51 const headers = new Headers(response.headers); 52 for (const [name, value] of Object.entries(CORS_HEADERS)) { 53 headers.set(name, value); 54 } 55 56 // Responses with these statuses must not carry a body; a non-null body 57 // crashes workerd, so null it out. 58 const body = [101, 204, 205, 304].includes(response.status) 59 ? null 60 : response.body; 61 62 return new Response(body, { 63 status: response.status, 64 statusText: response.statusText, 65 headers, 66 }); 67} 68 69export default { 70 async fetch(request: Request, env: Env): Promise<Response> { 71 if (request.method === "OPTIONS") { 72 return new Response(null, { status: 204, headers: CORS_HEADERS }); 73 } 74 75 const url = new URL(request.url); 76 if (url.pathname === "/" || url.pathname === "") { 77 return withCors( 78 new Response(INDEX, { headers: { "Content-Type": "text/plain" } }), 79 ); 80 } 81 82 const ip = request.headers.get("cf-connecting-ip") ?? "unknown"; 83 const { success } = await env.RATE_LIMITER.limit({ key: ip }); 84 if (!success) { 85 return withCors( 86 new Response( 87 JSON.stringify({ 88 error: "RateLimitExceeded", 89 message: "too many requests, slow down", 90 }), 91 { 92 status: 429, 93 headers: { 94 "Content-Type": "application/json", 95 "Retry-After": "60", 96 }, 97 }, 98 ), 99 ); 100 } 101 102 const container = env.BOBBIN.getByName("primary-eu-v2", { 103 locationHint: "weur", 104 }); 105 const response = await container.fetch(request); 106 // A 101 is a protocol switch (e.g. WebSocket upgrade); return it untouched 107 // so we don't strip the connection off the response. 108 if (response.status === 101) { 109 return response; 110 } 111 return withCors(response); 112 }, 113} satisfies ExportedHandler<Env>;