Monorepo for Tangled tangled.org
2

Configure Feed

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

bobbin/worker: add cloudflare containers deployment

Signed-off-by: Anirudh Oppiliappan <x@icyphox.sh>

author
Anirudh Oppiliappan
committer
Anirudh Oppiliappan
date (Jun 18, 2026, 1:00 PM +0300) commit e4346367 parent 898de9c1 change-id lqyotqvt
+127
+20
bobbin/worker/package.json
··· 1 + { 2 + "name": "@tangled/bobbin-worker", 3 + "version": "1.0.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "wrangler dev", 8 + "deploy": "wrangler deploy", 9 + "typecheck": "tsc --noEmit" 10 + }, 11 + "dependencies": { 12 + "@cloudflare/containers": "^0.3.7" 13 + }, 14 + "devDependencies": { 15 + "@cloudflare/workers-types": "^4.20260317.1", 16 + "@types/node": "^25.5.0", 17 + "typescript": "^5.9.3", 18 + "wrangler": "^4.75.0" 19 + } 20 + }
+49
bobbin/worker/src/index.ts
··· 1 + import { Container } from "@cloudflare/containers"; 2 + 3 + export interface Env { 4 + BOBBIN: DurableObjectNamespace<BobbinContainer>; 5 + BOBBIN_HYDRANT_URL: string; 6 + BOBBIN_SLINGSHOT_URL: string; 7 + BOBBIN_LOG: string; 8 + } 9 + 10 + export class BobbinContainer extends Container { 11 + defaultPort = 8090; 12 + enableInternet = true; 13 + // Bobbin maintains an in-memory index rebuilt from Hydrant replay on every 14 + // restart, so we want to avoid sleeping where possible. Override 15 + // onActivityExpired() to keep the container alive indefinitely. 16 + sleepAfter = "24h"; 17 + 18 + onActivityExpired(): boolean { 19 + // Keep the container running; bobbin's in-memory index is expensive to rebuild. 20 + return true; 21 + } 22 + 23 + onError(error: Error) { 24 + console.error("bobbin container error:", error); 25 + } 26 + } 27 + 28 + const INDEX = `This is bobbin, Tangled's stateless XRPC API service: https://tangled.org/tangled.org/core/tree/master/bobbin`; 29 + 30 + export default { 31 + async fetch(request: Request, env: Env): Promise<Response> { 32 + const url = new URL(request.url); 33 + if (url.pathname === "/" || url.pathname === "") { 34 + return new Response(INDEX, { headers: { "Content-Type": "text/plain" } }); 35 + } 36 + const container = env.BOBBIN.getByName("primary"); 37 + await container.startAndWaitForPorts({ 38 + startOptions: { 39 + envVars: { 40 + BOBBIN_HYDRANT_URL: env.BOBBIN_HYDRANT_URL, 41 + BOBBIN_SLINGSHOT_URL: env.BOBBIN_SLINGSHOT_URL, 42 + BOBBIN_LOG: env.BOBBIN_LOG, 43 + BOBBIN_LOG_FORMAT: "json", 44 + }, 45 + }, 46 + }); 47 + return container.fetch(request); 48 + }, 49 + } satisfies ExportedHandler<Env>;
+16
bobbin/worker/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ES2022", 5 + "lib": ["ES2022"], 6 + "moduleResolution": "bundler", 7 + "types": ["@cloudflare/workers-types", "node"], 8 + "strict": true, 9 + "esModuleInterop": true, 10 + "skipLibCheck": true, 11 + "forceConsistentCasingInFileNames": true, 12 + "noEmit": true 13 + }, 14 + "include": ["src/**/*"], 15 + "exclude": ["node_modules"] 16 + }
+42
bobbin/worker/wrangler.jsonc
··· 1 + { 2 + "$schema": "node_modules/wrangler/config-schema.json", 3 + "name": "bobbin", 4 + "main": "src/index.ts", 5 + "compatibility_date": "2026-03-07", 6 + "observability": { 7 + "enabled": true, 8 + }, 9 + "containers": [ 10 + { 11 + "class_name": "BobbinContainer", 12 + "image": "../containerfiles/bobbin.Containerfile", 13 + "image_build_context": "../../", 14 + "instance_type": "standard-2", 15 + "max_instances": 1, 16 + }, 17 + ], 18 + "durable_objects": { 19 + "bindings": [ 20 + { 21 + "name": "BOBBIN", 22 + "class_name": "BobbinContainer", 23 + }, 24 + ], 25 + }, 26 + "migrations": [ 27 + { 28 + "tag": "v1", 29 + "new_sqlite_classes": ["BobbinContainer"], 30 + }, 31 + ], 32 + "custom_domains": [ 33 + { 34 + "pattern": "api.tangled.org", 35 + }, 36 + ], 37 + "vars": { 38 + "BOBBIN_HYDRANT_URL": "https://hyd.bob.oyster.cafe", 39 + "BOBBIN_SLINGSHOT_URL": "https://slng.bob.oyster.cafe", 40 + "BOBBIN_LOG": "info", 41 + }, 42 + }