2025-06-19 15:55:35 -07:00
|
|
|
/**
|
|
|
|
* SERVERUS SERVER module
|
|
|
|
* @module
|
|
|
|
*/
|
|
|
|
|
2025-06-19 15:43:01 -07:00
|
|
|
import * as colors from '@std/fmt/colors';
|
|
|
|
import * as path from '@std/path';
|
|
|
|
|
2025-06-22 14:19:16 -07:00
|
|
|
const DEFAULT_HANDLER_DIRECTORIES = [import.meta.resolve('./handlers')];
|
2025-06-22 14:22:23 -07:00
|
|
|
|
2025-06-19 15:55:35 -07:00
|
|
|
/** A `HANDLER` must take a `Request` and return a `Response` if it can handle it. */
|
2025-06-19 15:43:01 -07:00
|
|
|
type HANDLER = (request: Request) => Promise<Response | null | undefined> | Response | null | undefined;
|
2025-06-19 15:55:35 -07:00
|
|
|
|
|
|
|
/** A `LOGGER` must take a `Request`, a `Response`, and a `processing_time` and log it. */
|
2025-06-19 15:43:01 -07:00
|
|
|
type LOGGER = (request: Request, response: Response, processing_time: number) => void | Promise<void>;
|
|
|
|
|
2025-06-19 15:55:35 -07:00
|
|
|
/** A `HANDLER_MODULE` must export a default method and may export an unload method to be called at shutdown. */
|
2025-06-19 15:43:01 -07:00
|
|
|
interface HANDLER_MODULE {
|
|
|
|
default: HANDLER;
|
|
|
|
unload?: () => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Interface defining the configuration for a serverus server
|
|
|
|
*
|
|
|
|
* @property {string} [hostname='localhost'] - hostname to bind to
|
|
|
|
* @property {number} [port=8000] - port to bind to
|
|
|
|
* @property {logging} [logging=true] - true/false or a LOGGER instance, default: true (uses default logging)
|
|
|
|
*/
|
|
|
|
export type SERVER_OPTIONS = {
|
|
|
|
hostname?: string;
|
|
|
|
port?: number;
|
|
|
|
logging?: boolean | LOGGER;
|
2025-06-25 17:06:05 -07:00
|
|
|
root?: string;
|
2025-06-25 16:53:45 -07:00
|
|
|
watch?: boolean;
|
2025-06-19 15:43:01 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default options for a serverus server.
|
|
|
|
*
|
|
|
|
* @property {string} hostname - localhost
|
|
|
|
* @property {number} port - 8000
|
|
|
|
* @property {boolean} logging - true (use default logger)
|
|
|
|
*/
|
|
|
|
export const DEFAULT_SERVER_OPTIONS: SERVER_OPTIONS = {
|
|
|
|
hostname: 'localhost',
|
|
|
|
port: 8000,
|
|
|
|
logging: true
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default logger
|
|
|
|
*
|
|
|
|
* @param {Request} request - the incoming request
|
|
|
|
* @param {Response} response - the outgoing response
|
|
|
|
* @param {number} time - the elapsed time in ms since the request was received
|
|
|
|
*/
|
|
|
|
function LOG_REQUEST(request: Request, response: Response, time: number) {
|
|
|
|
const c = response.status >= 500 ? colors.red : response.status >= 400 ? colors.yellow : colors.green;
|
|
|
|
const u = new URL(request.url);
|
|
|
|
const qs = u.searchParams.toString();
|
|
|
|
console.log(
|
|
|
|
`${c(request.method)} ${colors.gray(`(${response.status})`)} - ${
|
|
|
|
colors.cyan(
|
|
|
|
`${u.pathname}${qs ? '?' + qs : ''}`
|
|
|
|
)
|
|
|
|
} - ${colors.bold(String(time) + 'ms')}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* serverus SERVER
|
|
|
|
*
|
|
|
|
* Loads all handlers found in the [semi-]colon separated list of directories in
|
|
|
|
*/
|
|
|
|
export class SERVER {
|
|
|
|
private options: SERVER_OPTIONS;
|
|
|
|
private server: Deno.HttpServer | undefined;
|
|
|
|
private controller: AbortController | undefined;
|
|
|
|
private shutdown_binding: (() => void) | undefined;
|
|
|
|
private handlers: HANDLER_MODULE[];
|
2025-06-25 17:06:05 -07:00
|
|
|
private original_directory: string | undefined;
|
2025-06-19 15:43:01 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {SERVER_OPTIONS} (optional) options to configure the server
|
|
|
|
*/
|
|
|
|
constructor(options?: SERVER_OPTIONS) {
|
|
|
|
this.options = {
|
|
|
|
...DEFAULT_SERVER_OPTIONS,
|
|
|
|
...(options ?? {})
|
|
|
|
};
|
|
|
|
this.handlers = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
private shutdown() {
|
|
|
|
this.controller?.abort();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the server.
|
|
|
|
*
|
|
|
|
* @returns {Promise<SERVER>}
|
|
|
|
*/
|
|
|
|
public async start(): Promise<SERVER> {
|
2025-06-25 17:06:05 -07:00
|
|
|
const root = this.options.root ?? Deno.env.get('SERVERUS_ROOT');
|
|
|
|
if (typeof root === 'string') {
|
|
|
|
this.original_directory = Deno.cwd();
|
|
|
|
Deno.chdir(path.resolve(root));
|
|
|
|
}
|
|
|
|
|
2025-06-19 15:43:01 -07:00
|
|
|
this.controller = new AbortController();
|
|
|
|
const { signal } = this.controller;
|
|
|
|
|
2025-06-24 12:06:09 -07:00
|
|
|
const HANDLERS_DIRECTORIES: string[] = Deno.env.get('SERVERUS_HANDLERS')
|
|
|
|
?.split(/[;]/g)
|
|
|
|
?.filter((dir) => dir.length > 0)
|
|
|
|
?.map((dir) => {
|
|
|
|
return dir.includes('://') ? dir : path.resolve(dir);
|
|
|
|
}) ?? DEFAULT_HANDLER_DIRECTORIES;
|
2025-06-19 15:43:01 -07:00
|
|
|
|
|
|
|
for (const handler_directory of HANDLERS_DIRECTORIES) {
|
2025-06-22 14:19:16 -07:00
|
|
|
const index_file = path.join(handler_directory, 'index.ts');
|
2025-06-22 14:10:37 -07:00
|
|
|
|
|
|
|
try {
|
|
|
|
const handlers_index = await import(index_file);
|
2025-06-22 14:22:23 -07:00
|
|
|
|
2025-06-22 14:10:37 -07:00
|
|
|
for (const handler_name of Object.keys(handlers_index.default)) {
|
|
|
|
const handler_module: HANDLER_MODULE = handlers_index.default[handler_name];
|
|
|
|
if (typeof handler_module?.default !== 'function') {
|
|
|
|
console.warn(`Could not load handler "${handler_name}" - no default exported function`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.handlers.push(handler_module);
|
2025-06-19 15:43:01 -07:00
|
|
|
}
|
2025-06-22 14:10:37 -07:00
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof Deno.errors.NotFound) {
|
|
|
|
console.warn(`Could not load handler index from: ${index_file}`);
|
2025-06-19 15:43:01 -07:00
|
|
|
continue;
|
|
|
|
}
|
2025-06-22 14:10:37 -07:00
|
|
|
throw error;
|
2025-06-19 15:43:01 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.handlers.length === 0) {
|
|
|
|
throw new Error(`Could not load any handlers from: ${HANDLERS_DIRECTORIES.join('; ')}`, {
|
|
|
|
cause: 'no_handlers_loaded'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this.server = Deno.serve(
|
|
|
|
{
|
|
|
|
port: this.options.port ?? DEFAULT_SERVER_OPTIONS.port,
|
|
|
|
hostname: this.options.hostname ?? DEFAULT_SERVER_OPTIONS.hostname,
|
|
|
|
onError: (error: unknown) => {
|
2025-06-19 17:29:45 -07:00
|
|
|
console.error(error);
|
2025-06-19 15:43:01 -07:00
|
|
|
return Response.json({ error: { message: (error as Error).message } }, { status: 500 });
|
|
|
|
},
|
|
|
|
signal
|
|
|
|
},
|
|
|
|
async (request: Request): Promise<Response> => {
|
|
|
|
const request_time = Date.now();
|
|
|
|
const logger: LOGGER | undefined = typeof this.options.logging === 'function'
|
|
|
|
? this.options.logging
|
|
|
|
: (this.options.logging ? LOG_REQUEST : undefined);
|
|
|
|
|
|
|
|
for (const handler_module of this.handlers) {
|
|
|
|
const response = await handler_module.default(request);
|
|
|
|
if (response) {
|
|
|
|
logger?.(request, response, Date.now() - request_time);
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const not_found = Response.json(
|
|
|
|
{ error: { message: 'Not found', cause: 'not_found' } },
|
|
|
|
{ status: 404 }
|
|
|
|
);
|
|
|
|
logger?.(request, not_found, Date.now() - request_time);
|
|
|
|
return not_found;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
this.shutdown_binding = this.shutdown.bind(this);
|
|
|
|
Deno.addSignalListener('SIGTERM', this.shutdown_binding);
|
|
|
|
Deno.addSignalListener('SIGINT', this.shutdown_binding);
|
|
|
|
|
2025-06-25 16:53:45 -07:00
|
|
|
if (this.options.watch) {
|
|
|
|
Deno.watchFs;
|
|
|
|
}
|
2025-06-19 15:43:01 -07:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop the server
|
|
|
|
*/
|
|
|
|
public async stop(): Promise<void> {
|
|
|
|
if (this.server) {
|
|
|
|
this.controller?.abort();
|
|
|
|
await this.server.shutdown();
|
|
|
|
|
|
|
|
if (this.shutdown_binding) {
|
|
|
|
Deno.removeSignalListener('SIGTERM', this.shutdown_binding);
|
|
|
|
Deno.removeSignalListener('SIGINT', this.shutdown_binding);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-25 17:06:05 -07:00
|
|
|
if (this.original_directory) {
|
|
|
|
Deno.chdir(this.original_directory);
|
|
|
|
}
|
|
|
|
|
2025-06-19 15:43:01 -07:00
|
|
|
this.controller = undefined;
|
|
|
|
this.server = undefined;
|
|
|
|
this.shutdown_binding = undefined;
|
2025-06-25 17:06:05 -07:00
|
|
|
this.original_directory = undefined;
|
2025-06-19 15:43:01 -07:00
|
|
|
|
|
|
|
for (const handler_module of this.handlers) {
|
|
|
|
if (typeof handler_module.unload === 'function') {
|
|
|
|
handler_module.unload();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.handlers = [];
|
|
|
|
}
|
|
|
|
}
|