the statusphere demo reworked into a vite/react app in a monorepo
at main 145 lines 4.1 kB view raw
1import events from 'node:events' 2import fs from 'node:fs' 3import type http from 'node:http' 4import path from 'node:path' 5import { DAY, SECOND } from '@atproto/common' 6import compression from 'compression' 7import cors from 'cors' 8import express from 'express' 9import { pino } from 'pino' 10 11import API, { health, oauth } from '#/api' 12import { createClient } from '#/auth/client' 13import { AppContext } from '#/context' 14import { createDb, migrateToLatest } from '#/db' 15import * as error from '#/error' 16import { createBidirectionalResolver, createIdResolver } from '#/id-resolver' 17import { createFirehoseIngester, createJetstreamIngester } from '#/ingestors' 18import { createServer } from '#/lexicons' 19import { env } from '#/lib/env' 20 21export class Server { 22 constructor( 23 public app: express.Application, 24 public server: http.Server, 25 public ctx: AppContext, 26 ) {} 27 28 static async create() { 29 const { NODE_ENV, HOST, PORT, DB_PATH } = env 30 const logger = pino({ name: 'server start' }) 31 32 // Set up the SQLite database 33 const db = createDb(DB_PATH) 34 await migrateToLatest(db) 35 36 // Create the atproto utilities 37 const oauthClient = await createClient(db) 38 const baseIdResolver = createIdResolver() 39 const ingester = await createJetstreamIngester(db) 40 // Alternative: const ingester = await createFirehoseIngester(db, baseIdResolver) 41 const resolver = createBidirectionalResolver(baseIdResolver) 42 const ctx = { 43 db, 44 ingester, 45 logger, 46 oauthClient, 47 resolver, 48 } 49 50 // Subscribe to events on the firehose 51 ingester.start() 52 53 const app = express() 54 app.use(cors({ maxAge: DAY / SECOND })) 55 app.use(compression()) 56 app.use(express.json()) 57 app.use(express.urlencoded({ extended: true })) 58 59 // Create our server 60 let server = createServer({ 61 validateResponse: env.isDevelopment, 62 payload: { 63 jsonLimit: 100 * 1024, // 100kb 64 textLimit: 100 * 1024, // 100kb 65 // no blobs 66 blobLimit: 0, 67 }, 68 }) 69 70 server = API(server, ctx) 71 72 app.use(health.createRouter(ctx)) 73 app.use(oauth.createRouter(ctx)) 74 app.use(server.xrpc.router) 75 app.use(error.createHandler(ctx)) 76 77 // Serve static files from the frontend build - prod only 78 if (env.isProduction) { 79 const frontendPath = path.resolve( 80 __dirname, 81 '../../../packages/client/dist', 82 ) 83 84 // Check if the frontend build exists 85 if (fs.existsSync(frontendPath)) { 86 logger.info(`Serving frontend static files from: ${frontendPath}`) 87 88 // Serve static files 89 app.use(express.static(frontendPath)) 90 91 // For any other requests, send the index.html file 92 app.get('*', (req, res) => { 93 // Only handle non-API paths 94 if (!req.path.startsWith('/xrpc/')) { 95 res.sendFile(path.join(frontendPath, 'index.html')) 96 } else { 97 res.status(404).json({ error: 'API endpoint not found' }) 98 } 99 }) 100 } else { 101 logger.warn(`Frontend build not found at: ${frontendPath}`) 102 app.use('*', (_req, res) => { 103 res.sendStatus(404) 104 }) 105 } 106 } else { 107 app.set('trust proxy', true) 108 } 109 110 // Use the port from env (should be 3001 for the API server) 111 const httpServer = app.listen(env.PORT) 112 await events.once(httpServer, 'listening') 113 logger.info( 114 `API Server (${NODE_ENV}) running on port http://${HOST}:${env.PORT}`, 115 ) 116 117 return new Server(app, httpServer, ctx) 118 } 119 120 async close() { 121 this.ctx.logger.info('sigint received, shutting down') 122 await this.ctx.ingester.destroy() 123 await new Promise<void>((resolve) => { 124 this.server.close(() => { 125 this.ctx.logger.info('server closed') 126 resolve() 127 }) 128 }) 129 } 130} 131 132const run = async () => { 133 const server = await Server.create() 134 135 const onCloseSignal = async () => { 136 setTimeout(() => process.exit(1), 10000).unref() // Force shutdown after 10s 137 await server.close() 138 process.exit(0) 139 } 140 141 process.on('SIGINT', onCloseSignal) 142 process.on('SIGTERM', onCloseSignal) 143} 144 145run()