WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
4
fork

Configure Feed

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

at e749db1438a06bfdd8a9d9095f4d63a36cf9bf73 143 lines 4.4 kB view raw
1import type { Database } from "@atbb/db"; 2import { createDb } from "@atbb/db"; 3import type { Logger } from "@atbb/logger"; 4import { createLogger } from "@atbb/logger"; 5import { FirehoseService } from "./firehose.js"; 6import { NodeOAuthClient } from "@atproto/oauth-client-node"; 7import { OAuthStateStore, OAuthSessionStore } from "./oauth-stores.js"; 8import { CookieSessionStore } from "./cookie-session-store.js"; 9import { ForumAgent } from "@atbb/atproto"; 10import type { AppConfig } from "./config.js"; 11import { BackfillManager } from "./backfill-manager.js"; 12 13/** 14 * Application context holding all shared dependencies. 15 * This interface defines the contract for dependency injection. 16 */ 17export interface AppContext { 18 config: AppConfig; 19 logger: Logger; 20 db: Database; 21 firehose: FirehoseService; 22 oauthClient: NodeOAuthClient; 23 oauthStateStore: OAuthStateStore; 24 oauthSessionStore: OAuthSessionStore; 25 cookieSessionStore: CookieSessionStore; 26 forumAgent: ForumAgent | null; 27 backfillManager: BackfillManager | null; 28} 29 30/** 31 * Create and initialize the application context with all dependencies. 32 * This is the composition root where we wire up all dependencies. 33 */ 34export async function createAppContext(config: AppConfig): Promise<AppContext> { 35 const logger = createLogger({ 36 service: "atbb-appview", 37 version: "0.1.0", 38 environment: process.env.NODE_ENV ?? "development", 39 level: config.logLevel, 40 }); 41 42 const db = createDb(config.databaseUrl); 43 const firehose = new FirehoseService(db, config.jetstreamUrl, logger); 44 45 // Initialize OAuth stores 46 const oauthStateStore = new OAuthStateStore(); 47 const oauthSessionStore = new OAuthSessionStore(); 48 const cookieSessionStore = new CookieSessionStore(); 49 50 // Simple in-memory lock for single-instance deployments 51 // For multi-instance production, use Redis-based locking (e.g., with redlock) 52 const locks = new Map<string, Promise<unknown>>(); 53 const requestLock = async <T>(key: string, fn: () => T | PromiseLike<T>): Promise<T> => { 54 // Wait for any existing lock on this key 55 while (locks.has(key)) { 56 await locks.get(key); 57 } 58 59 // Acquire lock 60 const promise = Promise.resolve(fn()); 61 locks.set(key, promise); 62 63 try { 64 return await promise; 65 } finally { 66 // Release lock 67 locks.delete(key); 68 } 69 }; 70 71 // Replace localhost with 127.0.0.1 for RFC 8252 compliance 72 const oauthUrl = config.oauthPublicUrl.replace('localhost', '127.0.0.1'); 73 74 // Initialize OAuth client with configuration 75 const oauthClient = new NodeOAuthClient({ 76 clientMetadata: { 77 client_id: `${oauthUrl}/.well-known/oauth-client-metadata`, 78 client_name: "atBB Forum", 79 client_uri: oauthUrl, 80 redirect_uris: [`${oauthUrl}/api/auth/callback`], 81 scope: "atproto transition:generic", 82 grant_types: ["authorization_code", "refresh_token"], 83 response_types: ["code"], 84 application_type: "web", 85 token_endpoint_auth_method: "none", 86 dpop_bound_access_tokens: true, 87 }, 88 stateStore: oauthStateStore, 89 sessionStore: oauthSessionStore, 90 requestLock, 91 // Allow HTTP for development (never use in production!) 92 allowHttp: process.env.NODE_ENV !== "production", 93 }); 94 95 // Initialize ForumAgent (soft failure - never throws) 96 let forumAgent: ForumAgent | null = null; 97 if (config.forumHandle && config.forumPassword) { 98 forumAgent = new ForumAgent( 99 config.pdsUrl, 100 config.forumHandle, 101 config.forumPassword, 102 logger 103 ); 104 await forumAgent.initialize(); 105 } else { 106 logger.warn("ForumAgent credentials missing", { 107 operation: "createAppContext", 108 reason: "Missing FORUM_HANDLE or FORUM_PASSWORD environment variables", 109 }); 110 } 111 112 return { 113 config, 114 logger, 115 db, 116 firehose, 117 oauthClient, 118 oauthStateStore, 119 oauthSessionStore, 120 cookieSessionStore, 121 forumAgent, 122 backfillManager: new BackfillManager(db, config, logger), 123 }; 124} 125 126/** 127 * Cleanup and release resources held by the application context. 128 */ 129export async function destroyAppContext(ctx: AppContext): Promise<void> { 130 await ctx.firehose.stop(); 131 132 if (ctx.forumAgent) { 133 await ctx.forumAgent.shutdown(); 134 } 135 136 // Clean up OAuth store timers 137 ctx.oauthStateStore.destroy(); 138 ctx.oauthSessionStore.destroy(); 139 ctx.cookieSessionStore.destroy(); 140 141 // Flush pending log records and release OTel resources 142 await ctx.logger.shutdown(); 143}