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'
5
6import { createDb, migrateToLatest } from '#/db'
7import { env } from '#/env'
8import { Ingester } from '#/firehose/ingester'
9import { createRouter } from '#/routes'
10import { createClient } from '#/auth/client'
11import { createResolver } from '#/ident/resolver'
12import type { OAuthClient } from '@atproto/oauth-client-node'
13import type { Database } from '#/db'
14import { Resolver } from '#/ident/types'
15
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
34 const logger = pino({ name: 'server start' })
35 const db = createDb(DB_PATH)
36 await migrateToLatest(db)
37 const ingester = new Ingester(db)
38 const oauthClient = await createClient(db)
39 const resolver = createResolver()
40 ingester.start()
41 const ctx = {
42 db,
43 ingester,
44 logger,
45 oauthClient,
46 resolver,
47 }
48
49 const app: Express = express()
50
51 // Set the application to trust the reverse proxy
52 app.set('trust proxy', true)
53
54 // Middlewares
55 app.use(express.json())
56 app.use(express.urlencoded({ extended: true }))
57
58 // Routes
59 const router = createRouter(ctx)
60 app.use(router)
61
62 // Error handlers
63 app.use((_req, res) => res.sendStatus(404))
64
65 const server = app.listen(env.PORT)
66 await events.once(server, 'listening')
67 logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`)
68
69 return new Server(app, server, ctx)
70 }
71
72 async close() {
73 this.ctx.logger.info('sigint received, shutting down')
74 this.ctx.ingester.destroy()
75 return new Promise<void>((resolve) => {
76 this.server.close(() => {
77 this.ctx.logger.info('server closed')
78 resolve()
79 })
80 })
81 }
82}
83
84const run = async () => {
85 const server = await Server.create()
86
87 const onCloseSignal = async () => {
88 setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s
89 await server.close()
90 process.exit()
91 }
92
93 process.on('SIGINT', onCloseSignal)
94 process.on('SIGTERM', onCloseSignal)
95}
96
97run()