Files
xarticleaudio/src/server.js

188 lines
4.7 KiB
JavaScript

"use strict";
const http = require("node:http");
const { buildApp } = require("./app");
const { config } = require("./config");
const { ConvexStateStore } = require("./lib/convex-state-store");
const { InMemoryStateStore } = require("./lib/state-store");
const { createLogger } = require("./lib/logger");
function readBody(req) {
return new Promise((resolve, reject) => {
let data = "";
req.setEncoding("utf8");
req.on("data", (chunk) => {
data += chunk;
if (data.length > 2_000_000) {
reject(new Error("payload_too_large"));
}
});
req.on("end", () => resolve(data));
req.on("error", reject);
});
}
function normalizeHeaders(rawHeaders) {
return Object.fromEntries(
Object.entries(rawHeaders || {}).map(([k, v]) => [
String(k).toLowerCase(),
Array.isArray(v) ? v.join(",") : (v || ""),
]),
);
}
function mapToAppRequest({ req, rawBody }) {
const url = new URL(req.url, "http://localhost");
const query = Object.fromEntries(url.searchParams.entries());
return {
method: req.method || "GET",
path: url.pathname,
query,
headers: normalizeHeaders(req.headers),
rawBody,
};
}
function createHttpServer({ app }) {
return http.createServer(async (req, res) => {
try {
const rawBody = await readBody(req);
const response = await app.handleRequest(mapToAppRequest({ req, rawBody }));
res.statusCode = response.status;
for (const [key, value] of Object.entries(response.headers || {})) {
res.setHeader(key, value);
}
res.end(response.body || "");
} catch (error) {
res.statusCode = 500;
res.setHeader("content-type", "application/json; charset=utf-8");
res.end(JSON.stringify({ error: error.message || "internal_error" }));
}
});
}
function createMutationPersister({ stateStore, logger = console }) {
let queue = Promise.resolve();
let lastError = null;
return {
enqueue(state) {
queue = queue
.then(
() => stateStore.save(state),
() => stateStore.save(state),
)
.catch((error) => {
lastError = error;
logger.error({ err: error }, "failed to persist state");
throw error;
});
return queue;
},
flush() {
return queue;
},
getLastError() {
return lastError;
},
};
}
async function createRuntime({ runtimeConfig = config, logger = console, stateStore = null } = {}) {
let effectiveStateStore = stateStore || new ConvexStateStore({
deploymentUrl: runtimeConfig.convexDeploymentUrl,
authToken: runtimeConfig.convexAuthToken,
readFunction: runtimeConfig.convexStateQuery,
writeFunction: runtimeConfig.convexStateMutation,
});
let initialState;
try {
initialState = await effectiveStateStore.load();
} catch (error) {
const allowFallback = runtimeConfig.allowInMemoryStateFallback !== undefined
? Boolean(runtimeConfig.allowInMemoryStateFallback)
: true;
if (!allowFallback) {
throw new Error("state_store_unavailable_without_fallback", { cause: error });
}
logger.warn({ err: error }, "failed to initialize configured state store; falling back to in-memory state");
effectiveStateStore = new InMemoryStateStore();
initialState = await effectiveStateStore.load();
}
const persister = createMutationPersister({ stateStore: effectiveStateStore, logger });
const app = buildApp({
config: runtimeConfig,
initialState,
logger,
onMutation(state) {
void persister.enqueue(state);
},
});
const server = createHttpServer({ app });
return {
app,
server,
persister,
};
}
async function start() {
const logger = createLogger({
level: config.logLevel,
name: "xartaudio",
});
const runtime = await createRuntime({ runtimeConfig: config, logger });
const { server, persister } = runtime;
let shuttingDown = false;
async function shutdown(signal) {
if (shuttingDown) {
return;
}
shuttingDown = true;
logger.info({ signal }, "received shutdown signal");
server.close();
await persister.flush();
}
process.on("SIGTERM", () => {
void shutdown("SIGTERM");
});
process.on("SIGINT", () => {
void shutdown("SIGINT");
});
server.listen(config.port, () => {
logger.info({ port: config.port }, "xartaudio server listening");
});
}
if (require.main === module) {
start().catch((error) => {
const logger = createLogger({
level: config.logLevel,
name: "xartaudio",
});
logger.error({ err: error }, "failed to start server");
process.exitCode = 1;
});
}
module.exports = {
mapToAppRequest,
normalizeHeaders,
createHttpServer,
createMutationPersister,
createRuntime,
start,
};