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