diff --git a/bobbin/worker/package.json b/bobbin/worker/package.json new file mode 100644 index 00000000..476a58a1 --- /dev/null +++ b/bobbin/worker/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tangled/bobbin-worker", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cloudflare/containers": "^0.3.7" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260317.1", + "@types/node": "^25.5.0", + "typescript": "^5.9.3", + "wrangler": "^4.75.0" + } +} diff --git a/bobbin/worker/src/index.ts b/bobbin/worker/src/index.ts new file mode 100644 index 00000000..861a4ecf --- /dev/null +++ b/bobbin/worker/src/index.ts @@ -0,0 +1,49 @@ +import { Container } from "@cloudflare/containers"; + +export interface Env { + BOBBIN: DurableObjectNamespace; + BOBBIN_HYDRANT_URL: string; + BOBBIN_SLINGSHOT_URL: string; + BOBBIN_LOG: string; +} + +export class BobbinContainer extends Container { + defaultPort = 8090; + enableInternet = true; + // Bobbin maintains an in-memory index rebuilt from Hydrant replay on every + // restart, so we want to avoid sleeping where possible. Override + // onActivityExpired() to keep the container alive indefinitely. + sleepAfter = "24h"; + + onActivityExpired(): boolean { + // Keep the container running; bobbin's in-memory index is expensive to rebuild. + return true; + } + + onError(error: Error) { + console.error("bobbin container error:", error); + } +} + +const INDEX = `This is bobbin, Tangled's stateless XRPC API service: https://tangled.org/tangled.org/core/tree/master/bobbin`; + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + if (url.pathname === "/" || url.pathname === "") { + return new Response(INDEX, { headers: { "Content-Type": "text/plain" } }); + } + const container = env.BOBBIN.getByName("primary"); + await container.startAndWaitForPorts({ + startOptions: { + envVars: { + BOBBIN_HYDRANT_URL: env.BOBBIN_HYDRANT_URL, + BOBBIN_SLINGSHOT_URL: env.BOBBIN_SLINGSHOT_URL, + BOBBIN_LOG: env.BOBBIN_LOG, + BOBBIN_LOG_FORMAT: "json", + }, + }, + }); + return container.fetch(request); + }, +} satisfies ExportedHandler; diff --git a/bobbin/worker/tsconfig.json b/bobbin/worker/tsconfig.json new file mode 100644 index 00000000..6969b0ec --- /dev/null +++ b/bobbin/worker/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "types": ["@cloudflare/workers-types", "node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/bobbin/worker/wrangler.jsonc b/bobbin/worker/wrangler.jsonc new file mode 100644 index 00000000..92dd259c --- /dev/null +++ b/bobbin/worker/wrangler.jsonc @@ -0,0 +1,42 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "bobbin", + "main": "src/index.ts", + "compatibility_date": "2026-03-07", + "observability": { + "enabled": true, + }, + "containers": [ + { + "class_name": "BobbinContainer", + "image": "../containerfiles/bobbin.Containerfile", + "image_build_context": "../../", + "instance_type": "standard-2", + "max_instances": 1, + }, + ], + "durable_objects": { + "bindings": [ + { + "name": "BOBBIN", + "class_name": "BobbinContainer", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["BobbinContainer"], + }, + ], + "custom_domains": [ + { + "pattern": "api.tangled.org", + }, + ], + "vars": { + "BOBBIN_HYDRANT_URL": "https://hyd.bob.oyster.cafe", + "BOBBIN_SLINGSHOT_URL": "https://slng.bob.oyster.cafe", + "BOBBIN_LOG": "info", + }, +}