Spark feed generator template
1import { Hono } from "hono";
2import { configure, getConsoleSink, getLogger, Logger } from "@logtape/logtape";
3import { getPrettyFormatter } from "@logtape/pretty";
4import { Database } from "./db/connection.ts";
5import { DidResolver } from "@atp/identity";
6import { AuthVerifier } from "./utils/auth.ts";
7import { createServer } from "./lex/index.ts";
8import { env } from "./utils/env.ts";
9
10import describeFeedGenerator from "./api/describeFeedGenerator.ts";
11import getFeedSkeleton from "./api/getFeedSkeleton.ts";
12import wellKnown from "./api/well-known.ts";
13import health from "./api/health.ts";
14import { Ingester } from "./ingester/index.ts";
15
16await configure({
17 sinks: {
18 console: getConsoleSink({
19 formatter: getPrettyFormatter({
20 properties: true,
21 categoryStyle: "underline",
22 messageColor: "rgb(255, 255, 255)",
23 categoryColor: "rgb(255, 255, 255)",
24 messageStyle: "reset",
25 }),
26 }),
27 },
28 loggers: [
29 { category: "feedgen", lowestLevel: "info", sinks: ["console"] },
30 { category: ["logtape", "meta"], lowestLevel: "error", sinks: ["console"] },
31 ],
32});
33
34const logger = getLogger("feedgen");
35
36const ownDid = `did:web:${env.SPRK_FEEDGEN_DOMAIN}`;
37const didResolver = new DidResolver({});
38const authVerifier = new AuthVerifier(ownDid, didResolver);
39const db = new Database();
40
41const ctx = {
42 db,
43 logger,
44 ownDid,
45 didResolver,
46 authVerifier,
47};
48
49export type AppContext = {
50 db: Database;
51 logger: Logger;
52 ownDid: string;
53 didResolver: DidResolver;
54 authVerifier: AuthVerifier;
55};
56
57export type AppEnv = {
58 Bindings: AppContext;
59};
60
61const app = new Hono<AppEnv>();
62const server = createServer();
63describeFeedGenerator(server, ctx);
64getFeedSkeleton(server, ctx);
65
66app.route("/", server.xrpc.app);
67
68app.use("*", async (c, next) => {
69 // Initialize c.env if it doesn't exist (for testing compatibility)
70 if (!c.env) {
71 c.env = {} as AppContext;
72 }
73 c.env.ownDid = ownDid;
74 c.env.didResolver = didResolver;
75 c.env.authVerifier = authVerifier;
76 c.env.logger = logger;
77 await next();
78});
79
80app.get("/", (c) => {
81 return c.text(
82 "This is a Feed Generation service! Most endpoints are under /xrpc",
83 );
84});
85
86// Initialize database connection when server starts
87
88export async function startServer() {
89 logger.info("Starting Feed Generator service");
90
91 try {
92 await db.connect();
93 } catch (err) {
94 logger.error("Failed to connect to database", { err });
95 Deno.exit(1);
96 }
97
98 const ingester = new Ingester(db);
99 ingester.firehose.start();
100
101 const { SPRK_HOST, SPRK_PORT } = env;
102 Deno.serve({
103 hostname: SPRK_HOST,
104 port: SPRK_PORT,
105 onListen: (info) => {
106 logger.info(`Server listening on http://${info.hostname}:${info.port}`);
107 },
108 }, app.fetch);
109
110 // Handle shutdown gracefully
111 const shutdown = async () => {
112 logger.info("Shutting down...");
113 await db.disconnect();
114 Deno.exit(0);
115 };
116
117 Deno.addSignalListener("SIGINT", shutdown);
118 Deno.addSignalListener("SIGTERM", shutdown);
119
120 logger.info("Feed Generator service is running");
121
122 app.route("/.well-known", wellKnown);
123 app.route("/", health);
124
125 return app;
126}
127
128// For development and testing
129if (import.meta.main) {
130 startServer();
131}