188 lines
4.7 KiB
JavaScript
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,
|
|
};
|