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.4 kB 110 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 { 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 async onActivityExpired(): Promise<void> { 20 // Keep the container running; bobbin's in-memory index is expensive to 21 // rebuild. Renew the timeout instead of stopping so we're pinged again later. 22 this.renewActivityTimeout(); 23 } 24 25 onError(error: Error) { 26 console.error("bobbin container error:", error); 27 } 28} 29 30const INDEX = `This is bobbin, Tangled's stateless XRPC API service: https://tangled.org/tangled.org/core/tree/master/bobbin`; 31 32const CORS_HEADERS = { 33 "Access-Control-Allow-Origin": "*", 34 "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", 35 "Access-Control-Allow-Headers": "Content-Type, Authorization", 36 "Access-Control-Max-Age": "86400", 37}; 38 39function withCors(response: Response): Response { 40 const headers = new Headers(response.headers); 41 for (const [name, value] of Object.entries(CORS_HEADERS)) { 42 headers.set(name, value); 43 } 44 45 // Responses with these statuses must not carry a body; a non-null body 46 // crashes workerd, so null it out. 47 const body = [101, 204, 205, 304].includes(response.status) 48 ? null 49 : response.body; 50 51 return new Response(body, { 52 status: response.status, 53 statusText: response.statusText, 54 headers, 55 }); 56} 57 58export default { 59 async fetch(request: Request, env: Env): Promise<Response> { 60 if (request.method === "OPTIONS") { 61 return new Response(null, { status: 204, headers: CORS_HEADERS }); 62 } 63 64 const url = new URL(request.url); 65 if (url.pathname === "/" || url.pathname === "") { 66 return withCors( 67 new Response(INDEX, { headers: { "Content-Type": "text/plain" } }), 68 ); 69 } 70 71 const ip = request.headers.get("cf-connecting-ip") ?? "unknown"; 72 const { success } = await env.RATE_LIMITER.limit({ key: ip }); 73 if (!success) { 74 return withCors( 75 new Response( 76 JSON.stringify({ 77 error: "RateLimitExceeded", 78 message: "too many requests, slow down", 79 }), 80 { 81 status: 429, 82 headers: { 83 "Content-Type": "application/json", 84 "Retry-After": "60", 85 }, 86 }, 87 ), 88 ); 89 } 90 91 const container = env.BOBBIN.getByName("primary"); 92 await container.startAndWaitForPorts({ 93 startOptions: { 94 envVars: { 95 BOBBIN_HYDRANT_URL: env.BOBBIN_HYDRANT_URL, 96 BOBBIN_SLINGSHOT_URL: env.BOBBIN_SLINGSHOT_URL, 97 BOBBIN_LOG: env.BOBBIN_LOG, 98 BOBBIN_LOG_FORMAT: "json", 99 }, 100 }, 101 }); 102 const response = await container.fetch(request); 103 // A 101 is a protocol switch (e.g. WebSocket upgrade); return it untouched 104 // so we don't strip the connection off the response. 105 if (response.status === 101) { 106 return response; 107 } 108 return withCors(response); 109 }, 110} satisfies ExportedHandler<Env>;