Scratch space for learning atproto app development
1import events from 'node:events' 2import type http from 'node:http' 3import express, { type Express } from 'express' 4import { pino } from 'pino' 5import type { OAuthClient } from '@atproto/oauth-client-node' 6 7import { createDb, migrateToLatest } from '#/db' 8import { env } from '#/lib/env' 9import { Ingester } from '#/firehose/ingester' 10import { createRouter } from '#/routes' 11import { createClient } from '#/auth/client' 12import { createResolver, Resolver } from '#/firehose/resolver' 13import type { Database } from '#/db' 14 15// Application state passed to the router and elsewhere 16export type AppContext = { 17 db: Database 18 ingester: Ingester 19 logger: pino.Logger 20 oauthClient: OAuthClient 21 resolver: Resolver 22} 23 24export class Server { 25 constructor( 26 public app: express.Application, 27 public server: http.Server, 28 public ctx: AppContext 29 ) {} 30 31 static async create() { 32 const { NODE_ENV, HOST, PORT, DB_PATH } = env 33 const logger = pino({ name: 'server start' }) 34 35 // Set up the SQLite database 36 const db = createDb(DB_PATH) 37 await migrateToLatest(db) 38 39 // Create the atproto utilities 40 const oauthClient = await createClient(db) 41 const ingester = new Ingester(db) 42 const resolver = createResolver() 43 const ctx = { 44 db, 45 ingester, 46 logger, 47 oauthClient, 48 resolver, 49 } 50 51 // Subscribe to events on the firehose 52 ingester.start() 53 54 // Create our server 55 const app: Express = express() 56 app.set('trust proxy', true) 57 58 // Routes & middlewares 59 const router = createRouter(ctx) 60 app.use(express.json()) 61 app.use(express.urlencoded({ extended: true })) 62 app.use(router) 63 app.use((_req, res) => res.sendStatus(404)) 64 65 // Bind our server to the port 66 const server = app.listen(env.PORT) 67 await events.once(server, 'listening') 68 logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`) 69 70 return new Server(app, server, ctx) 71 } 72 73 async close() { 74 this.ctx.logger.info('sigint received, shutting down') 75 this.ctx.ingester.destroy() 76 return new Promise<void>((resolve) => { 77 this.server.close(() => { 78 this.ctx.logger.info('server closed') 79 resolve() 80 }) 81 }) 82 } 83} 84 85const run = async () => { 86 const server = await Server.create() 87 88 const onCloseSignal = async () => { 89 setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s 90 await server.close() 91 process.exit() 92 } 93 94 process.on('SIGINT', onCloseSignal) 95 process.on('SIGTERM', onCloseSignal) 96} 97 98run()