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.

refactor(appview): implement dependency injection pattern (#11)

Introduces a proper dependency injection pattern to make the appview
more testable and configurable. This change improves separation of
concerns and enables easier mocking in tests.

Changes:
- Add AppContext interface and factory (app-context.ts)
- Extract app creation logic to createApp() (create-app.ts)
- Add test helper createTestContext() (test-context.ts)
- Refactor index.ts to use composition root pattern
- Wrap startup in async main() for better error handling

Benefits:
- Dependencies can be easily swapped for testing
- Clear composition root at application startup
- Proper lifecycle management (creation and cleanup)
- More testable architecture with explicit dependencies

authored by

Malpercio and committed by
GitHub
61108c2c 5b97039d

+131 -49
+49 -49
apps/appview/src/index.ts
··· 1 - import { Hono } from "hono"; 2 1 import { serve } from "@hono/node-server"; 3 - import { logger } from "hono/logger"; 4 - import { apiRoutes } from "./routes/index.js"; 5 2 import { loadConfig } from "./lib/config.js"; 6 - import { FirehoseService } from "./lib/firehose.js"; 7 - import { createDb } from "@atbb/db"; 3 + import { createAppContext, destroyAppContext } from "./lib/app-context.js"; 4 + import { createApp } from "./lib/create-app.js"; 8 5 9 - const config = loadConfig(); 10 - const db = createDb(config.databaseUrl); 11 - const app = new Hono(); 6 + async function main() { 7 + // Load configuration 8 + const config = loadConfig(); 12 9 13 - app.use("*", logger()); 14 - app.route("/api", apiRoutes); 10 + // Create application context with all dependencies 11 + const ctx = await createAppContext(config); 15 12 16 - // Initialize firehose service 17 - const firehose = new FirehoseService(db, config.jetstreamUrl); 13 + // Create Hono app 14 + const app = createApp(ctx); 18 15 19 - // Start the server 20 - const server = serve( 21 - { 22 - fetch: app.fetch, 23 - port: config.port, 24 - }, 25 - (info) => { 26 - console.log(`atBB AppView listening on http://localhost:${info.port}`); 27 - } 28 - ); 16 + // Start HTTP server 17 + const server = serve( 18 + { 19 + fetch: app.fetch, 20 + port: config.port, 21 + }, 22 + (info) => { 23 + console.log(`atBB AppView listening on http://localhost:${info.port}`); 24 + } 25 + ); 29 26 30 - // Start the firehose subscription 31 - firehose.start().catch((error) => { 32 - console.error("Failed to start firehose:", error); 33 - process.exit(1); 34 - }); 27 + // Start firehose subscription 28 + ctx.firehose.start().catch((error) => { 29 + console.error("Failed to start firehose:", error); 30 + process.exit(1); 31 + }); 35 32 36 - // Handle graceful shutdown 37 - const shutdown = async (signal: string) => { 38 - console.log(`\nReceived ${signal}, shutting down gracefully...`); 33 + // Graceful shutdown handler 34 + const shutdown = async (signal: string) => { 35 + console.log(`\nReceived ${signal}, shutting down gracefully...`); 39 36 40 - try { 41 - // Stop the firehose 42 - await firehose.stop(); 37 + try { 38 + await destroyAppContext(ctx); 43 39 44 - // Close the server 45 - server.close(() => { 46 - console.log("Server closed"); 47 - process.exit(0); 48 - }); 40 + server.close(() => { 41 + console.log("Server closed"); 42 + process.exit(0); 43 + }); 49 44 50 - // Force exit after 10 seconds 51 - setTimeout(() => { 52 - console.error("Forced shutdown after timeout"); 45 + setTimeout(() => { 46 + console.error("Forced shutdown after timeout"); 47 + process.exit(1); 48 + }, 10000); 49 + } catch (error) { 50 + console.error("Error during shutdown:", error); 53 51 process.exit(1); 54 - }, 10000); 55 - } catch (error) { 56 - console.error("Error during shutdown:", error); 57 - process.exit(1); 58 - } 59 - }; 52 + } 53 + }; 60 54 61 - process.on("SIGTERM", () => shutdown("SIGTERM")); 62 - process.on("SIGINT", () => shutdown("SIGINT")); 55 + process.on("SIGTERM", () => shutdown("SIGTERM")); 56 + process.on("SIGINT", () => shutdown("SIGINT")); 57 + } 58 + 59 + main().catch((error) => { 60 + console.error("Fatal error during startup:", error); 61 + process.exit(1); 62 + });
+28
apps/appview/src/lib/__tests__/test-context.ts
··· 1 + import { createDb } from "@atbb/db"; 2 + import { FirehoseService } from "../firehose.js"; 3 + import type { AppContext } from "../app-context.js"; 4 + import type { AppConfig } from "../config.js"; 5 + 6 + /** 7 + * Create a test application context with in-memory database. 8 + * Useful for integration tests. 9 + */ 10 + export function createTestContext(overrides?: Partial<AppConfig>): AppContext { 11 + const config: AppConfig = { 12 + port: 3000, 13 + forumDid: "did:plc:test", 14 + pdsUrl: "https://test.pds", 15 + databaseUrl: process.env.DATABASE_URL ?? "", 16 + jetstreamUrl: "wss://test.jetstream", 17 + ...overrides, 18 + }; 19 + 20 + const db = createDb(config.databaseUrl); 21 + const firehose = new FirehoseService(db, config.jetstreamUrl); 22 + 23 + return { 24 + config, 25 + db, 26 + firehose, 27 + }; 28 + }
+37
apps/appview/src/lib/app-context.ts
··· 1 + import type { Database } from "@atbb/db"; 2 + import { createDb } from "@atbb/db"; 3 + import { FirehoseService } from "./firehose.js"; 4 + import type { AppConfig } from "./config.js"; 5 + 6 + /** 7 + * Application context holding all shared dependencies. 8 + * This interface defines the contract for dependency injection. 9 + */ 10 + export interface AppContext { 11 + config: AppConfig; 12 + db: Database; 13 + firehose: FirehoseService; 14 + } 15 + 16 + /** 17 + * Create and initialize the application context with all dependencies. 18 + * This is the composition root where we wire up all dependencies. 19 + */ 20 + export async function createAppContext(config: AppConfig): Promise<AppContext> { 21 + const db = createDb(config.databaseUrl); 22 + const firehose = new FirehoseService(db, config.jetstreamUrl); 23 + 24 + return { 25 + config, 26 + db, 27 + firehose, 28 + }; 29 + } 30 + 31 + /** 32 + * Cleanup and release resources held by the application context. 33 + */ 34 + export async function destroyAppContext(ctx: AppContext): Promise<void> { 35 + await ctx.firehose.stop(); 36 + // Future: close database connection when needed 37 + }
+17
apps/appview/src/lib/create-app.ts
··· 1 + import { Hono } from "hono"; 2 + import { logger } from "hono/logger"; 3 + import { apiRoutes } from "../routes/index.js"; 4 + import type { AppContext } from "./app-context.js"; 5 + 6 + /** 7 + * Create the Hono application with routes and middleware. 8 + * Routes can access the context via c.env or closure. 9 + */ 10 + export function createApp(ctx: AppContext) { 11 + const app = new Hono(); 12 + 13 + app.use("*", logger()); 14 + app.route("/api", apiRoutes); 15 + 16 + return app; 17 + }