[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { Hono } from "hono";
2import { cors } from "hono/cors";
3import { logger } from "hono/logger";
4import { Database } from "./data-plane/db/index.ts";
5import { createAuthVerifier } from "./auth-verifier.ts";
6import API from "./api/index.ts";
7import { createServer } from "./lex/index.ts";
8import wellKnown from "./api/well-known.ts";
9import health from "./api/health.ts";
10import { IdResolver } from "@atp/identity";
11import { DataPlane } from "./data-plane/index.ts";
12import { getLogger } from "@logtape/logtape";
13import { configureLogger } from "./utils/logger.ts";
14import { Hydrator } from "./hydration/index.ts";
15import { Views } from "./views/index.ts";
16import { AppContext, AppEnv } from "./context.ts";
17import { ServerConfig } from "./config.ts";
18import { defaultLabelerHeader, parseLabelerHeader } from "./util.ts";
19
20await configureLogger();
21
22// Create app without starting services
23export function createApp(ctx: AppContext): Hono<AppEnv> {
24 const app = new Hono<AppEnv>();
25
26 app.use("*", cors());
27 app.use("*", logger());
28 app.use("*", async (c, next) => {
29 c.env = ctx;
30 await next();
31 });
32
33 // Lexicon/XRPC server and routers
34 const lexServer = createServer();
35 API(lexServer, ctx);
36
37 app.route("/.well-known", wellKnown);
38 app.route("/", health);
39 app.route("/", lexServer.xrpc.app);
40
41 return app;
42}
43
44// Setup function to create context and app
45export function setupApp(): { app: Hono<AppEnv>; ctx: AppContext } {
46 // Setup logger and database
47 const appLogger = getLogger(["appview"]);
48 const cfg = ServerConfig.readEnv();
49 const db = new Database(cfg);
50 db.connect();
51
52 // DID and resolver setup
53 const idResolver = new IdResolver({ plcUrl: cfg.plcUrl });
54
55 const dataplane = new DataPlane(db, idResolver);
56 const hydrator = new Hydrator(dataplane, cfg.labelsFromIssuerDids);
57 const views = new Views({
58 indexedAtEpoch: cfg.indexedAtEpoch,
59 videoCdn: cfg.videoCdn,
60 mediaCdn: cfg.mediaCdn,
61 thumbCdn: cfg.thumbCdn,
62 });
63
64 const authVerifier = createAuthVerifier(dataplane, {
65 ownDid: cfg.serverDid,
66 alternateAudienceDids: [],
67 modServiceDid: cfg.modServiceDid,
68 adminPasses: cfg.adminPasswords,
69 });
70
71 const reqLabelers = (req: Request) => {
72 const val = req.headers.get("atproto-accept-labelers") ?? undefined;
73 const parsed = parseLabelerHeader(val);
74 if (!parsed) return defaultLabelerHeader(cfg.labelsFromIssuerDids);
75 return parsed;
76 };
77
78 const ctx = {
79 db,
80 dataplane,
81 hydrator,
82 views,
83 logger: appLogger,
84 idResolver,
85 cfg,
86 authVerifier,
87 reqLabelers,
88 };
89
90 const app = createApp(ctx);
91 return { app, ctx };
92}
93
94// Start server function
95export function startServer() {
96 const { app, ctx } = setupApp();
97
98 // Start HTTP server immediately
99 const { port } = ctx.cfg;
100 Deno.serve({
101 port,
102 onListen: (info) => {
103 ctx.logger.info(`Server listening on ${info.hostname}:${info.port}`);
104 },
105 }, app.fetch);
106
107 // Handle shutdown
108 const shutdown = async (signal: string) => {
109 ctx.logger.info(`Received ${signal}; shutting down...`);
110 try {
111 ctx.logger.info("Disconnecting database...");
112 await ctx.db.disconnect();
113 } catch (err) {
114 ctx.logger.error("Error disconnecting database during shutdown", { err });
115 }
116 ctx.logger.info("Shutdown complete");
117 Deno.exit(0);
118 };
119
120 Deno.addSignalListener("SIGINT", () => shutdown("SIGINT"));
121 Deno.addSignalListener("SIGTERM", () => shutdown("SIGTERM"));
122}
123
124// Start the server if this file is run directly
125if (import.meta.main) {
126 startServer();
127}