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..4f103451 --- /dev/null +++ b/bobbin/worker/src/index.ts @@ -0,0 +1,43 @@ +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); + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + 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..23b72e1a --- /dev/null +++ b/bobbin/worker/wrangler.jsonc @@ -0,0 +1,40 @@ +{ + "$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"], + }, + ], + "vars": { + // URL of the hydrant event stream service (ws:// or http:// — rewritten to ws at connect time) + "BOBBIN_HYDRANT_URL": "https://hyd.bob.oyster.cafe", + // URL of the slingshot record body storage service + "BOBBIN_SLINGSHOT_URL": "https://slng.bob.oyster.cafe", + // Tracing filter directive (e.g. "bobbin_xrpc=debug,info") + "BOBBIN_LOG": "info", + }, +}