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()