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