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
15export type AppContext = {
16 db: Database
17 ingester: Ingester
18 logger: pino.Logger
19 oauthClient: OAuthClient
20 resolver: Resolver
21}
22
23export class Server {
24 constructor(
25 public app: express.Application,
26 public server: http.Server,
27 public ctx: AppContext
28 ) {}
29
30 static async create() {
31 const { NODE_ENV, HOST, PORT, DB_PATH } = env
32
33 const logger = pino({ name: 'server start' })
34 const db = createDb(DB_PATH)
35 await migrateToLatest(db)
36 const ingester = new Ingester(db)
37 const oauthClient = await createClient(db)
38 const resolver = createResolver()
39 ingester.start()
40 const ctx = {
41 db,
42 ingester,
43 logger,
44 oauthClient,
45 resolver,
46 }
47
48 const app: Express = express()
49
50 // Set the application to trust the reverse proxy
51 app.set('trust proxy', true)
52
53 // Middlewares
54 app.use(express.json())
55 app.use(express.urlencoded({ extended: true }))
56
57 // Routes
58 const router = createRouter(ctx)
59 app.use(router)
60
61 // Error handlers
62 app.use((_req, res) => res.sendStatus(404))
63
64 const server = app.listen(env.PORT)
65 await events.once(server, 'listening')
66 logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`)
67
68 return new Server(app, server, ctx)
69 }
70
71 async close() {
72 this.ctx.logger.info('sigint received, shutting down')
73 this.ctx.ingester.destroy()
74 return new Promise<void>((resolve) => {
75 this.server.close(() => {
76 this.ctx.logger.info('server closed')
77 resolve()
78 })
79 })
80 }
81}
82
83const run = async () => {
84 const server = await Server.create()
85
86 const onCloseSignal = async () => {
87 setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s
88 await server.close()
89 process.exit()
90 }
91
92 process.on('SIGINT', onCloseSignal)
93 process.on('SIGTERM', onCloseSignal)
94}
95
96run()