feat: add fluxer upstream source and self-hosting documentation
- Clone of github.com/fluxerapp/fluxer (official upstream) - SELF_HOSTING.md: full VM rebuild procedure, architecture overview, service reference, step-by-step setup, troubleshooting, seattle reference - dev/.env.example: all env vars with secrets redacted and generation instructions - dev/livekit.yaml: LiveKit config template with placeholder keys - fluxer-seattle/: existing seattle deployment setup scripts
This commit is contained in:
148
fluxer/packages/hono/src/Server.tsx
Normal file
148
fluxer/packages/hono/src/Server.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createServer as createNodeServer, type IncomingMessage, type Server} from 'node:http';
|
||||
import type {Duplex} from 'node:stream';
|
||||
import {getRequestListener, type ServerType, serve} from '@hono/node-server';
|
||||
import type {Hono} from 'hono';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
hostname?: string;
|
||||
onListen?: (info: {address: string; port: number}) => void;
|
||||
}
|
||||
|
||||
export function createServer<E extends object = object>(app: Hono<E>, options: ServerOptions): ServerType {
|
||||
const {port, hostname, onListen} = options;
|
||||
|
||||
return serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
...(hostname !== undefined && {hostname}),
|
||||
},
|
||||
onListen,
|
||||
);
|
||||
}
|
||||
|
||||
export type UpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
||||
|
||||
export interface NodeServerOptions extends ServerOptions {
|
||||
onUpgrade?: UpgradeHandler;
|
||||
onServerCreated?: (server: Server) => void;
|
||||
}
|
||||
|
||||
function resolveServerAddress(
|
||||
server: Server,
|
||||
fallbackHost: string,
|
||||
fallbackPort: number,
|
||||
): {address: string; port: number} {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
return {address: fallbackHost, port: fallbackPort};
|
||||
}
|
||||
return {address: address.address, port: address.port};
|
||||
}
|
||||
|
||||
export function createServerWithUpgrade<E extends object = object>(app: Hono<E>, options: NodeServerOptions): Server {
|
||||
const {port, hostname, onListen, onUpgrade, onServerCreated} = options;
|
||||
const server = createNodeServer(getRequestListener(app.fetch));
|
||||
const fallbackHost = hostname ?? '0.0.0.0';
|
||||
|
||||
if (onUpgrade) {
|
||||
server.on('upgrade', (req, socket, head) => onUpgrade(req, socket, head));
|
||||
}
|
||||
|
||||
if (onServerCreated) {
|
||||
onServerCreated(server);
|
||||
}
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
if (onListen) {
|
||||
onListen(resolveServerAddress(server, fallbackHost, port));
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
export type CleanupFunction = () => void | Promise<void>;
|
||||
|
||||
export interface ShutdownLogger {
|
||||
info(msg: string): void;
|
||||
info(obj: Record<string, unknown>, msg: string): void;
|
||||
error(msg: string): void;
|
||||
error(obj: Record<string, unknown>, msg: string): void;
|
||||
}
|
||||
|
||||
const defaultShutdownLogger: ShutdownLogger = {
|
||||
info(objOrMsg: Record<string, unknown> | string, msg?: string): void {
|
||||
if (typeof objOrMsg === 'string') {
|
||||
process.stdout.write(`[info] ${objOrMsg}\n`);
|
||||
} else if (msg) {
|
||||
process.stdout.write(`[info] ${msg} ${JSON.stringify(objOrMsg)}\n`);
|
||||
}
|
||||
},
|
||||
error(objOrMsg: Record<string, unknown> | string, msg?: string): void {
|
||||
if (typeof objOrMsg === 'string') {
|
||||
process.stderr.write(`[error] ${objOrMsg}\n`);
|
||||
} else if (msg) {
|
||||
process.stderr.write(`[error] ${msg} ${JSON.stringify(objOrMsg)}\n`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export interface GracefulShutdownOptions {
|
||||
logger?: ShutdownLogger;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export function setupGracefulShutdown(cleanupFn: CleanupFunction, options?: GracefulShutdownOptions): void {
|
||||
const logger = options?.logger ?? defaultShutdownLogger;
|
||||
const timeoutMs = options?.timeoutMs;
|
||||
let isShuttingDown = false;
|
||||
|
||||
const shutdown = async (signal: string): Promise<void> => {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
isShuttingDown = true;
|
||||
logger.info({signal}, `Received ${signal}, shutting down gracefully...`);
|
||||
let timeoutHandle: NodeJS.Timeout | undefined;
|
||||
if (timeoutMs && timeoutMs > 0) {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
logger.error({timeoutMs}, 'Forcing shutdown after timeout');
|
||||
process.exit(1);
|
||||
}, timeoutMs);
|
||||
}
|
||||
try {
|
||||
await cleanupFn();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err: err instanceof Error ? err : new Error(String(err))}, 'Error during shutdown');
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
}
|
||||
Reference in New Issue
Block a user