initial
This commit is contained in:
45
ts/core/index.ts
Normal file
45
ts/core/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Main server class
|
||||
export { SmartServe } from './smartserve.classes.smartserve.js';
|
||||
|
||||
// Interfaces
|
||||
export type {
|
||||
// HTTP types
|
||||
THttpMethod,
|
||||
TRuntime,
|
||||
// Request/Response
|
||||
IRequestContext,
|
||||
TRouteHandler,
|
||||
IMethodOptions,
|
||||
IRouteOptions,
|
||||
// Interceptors
|
||||
TRequestInterceptor,
|
||||
TResponseInterceptor,
|
||||
TGuardFunction,
|
||||
IInterceptOptions,
|
||||
IGuardOptions,
|
||||
// WebSocket
|
||||
IWebSocketMessage,
|
||||
IWebSocketPeer,
|
||||
IWebSocketHooks,
|
||||
// Server config
|
||||
ITLSConfig,
|
||||
IKeepAliveConfig,
|
||||
IStaticOptions,
|
||||
IDirectoryListingOptions,
|
||||
IFileEntry,
|
||||
IWebDAVConfig,
|
||||
ISmartServeOptions,
|
||||
// Server instance
|
||||
IServerStats,
|
||||
ISmartServeInstance,
|
||||
IConnectionInfo,
|
||||
} from './smartserve.interfaces.js';
|
||||
|
||||
// Errors
|
||||
export {
|
||||
HttpError,
|
||||
RouteNotFoundError,
|
||||
UnsupportedRuntimeError,
|
||||
ServerAlreadyRunningError,
|
||||
ServerNotRunningError,
|
||||
} from './smartserve.errors.js';
|
||||
379
ts/core/smartserve.classes.smartserve.ts
Normal file
379
ts/core/smartserve.classes.smartserve.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* Main SmartServe server class
|
||||
* Cross-platform HTTP server with decorator-based routing
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.js';
|
||||
import type {
|
||||
ISmartServeOptions,
|
||||
ISmartServeInstance,
|
||||
IRequestContext,
|
||||
IConnectionInfo,
|
||||
THttpMethod,
|
||||
IInterceptOptions,
|
||||
TRequestInterceptor,
|
||||
TResponseInterceptor,
|
||||
} from './smartserve.interfaces.js';
|
||||
import { HttpError, RouteNotFoundError, ServerAlreadyRunningError } from './smartserve.errors.js';
|
||||
import { AdapterFactory, type BaseAdapter, type TRequestHandler } from '../adapters/index.js';
|
||||
import { ControllerRegistry, type ICompiledRoute } from '../decorators/index.js';
|
||||
import { FileServer } from '../files/index.js';
|
||||
import { WebDAVHandler } from '../protocols/index.js';
|
||||
|
||||
/**
|
||||
* SmartServe - Cross-platform HTTP server
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const server = new SmartServe({ port: 3000 });
|
||||
*
|
||||
* // Register decorated controllers
|
||||
* server.register(UserController);
|
||||
* server.register(ProductController);
|
||||
*
|
||||
* await server.start();
|
||||
* ```
|
||||
*/
|
||||
export class SmartServe {
|
||||
private options: ISmartServeOptions;
|
||||
private adapter: BaseAdapter | null = null;
|
||||
private instance: ISmartServeInstance | null = null;
|
||||
private customHandler: TRequestHandler | null = null;
|
||||
private fileServer: FileServer | null = null;
|
||||
private webdavHandler: WebDAVHandler | null = null;
|
||||
|
||||
constructor(options: ISmartServeOptions) {
|
||||
this.options = {
|
||||
hostname: '0.0.0.0',
|
||||
...options,
|
||||
};
|
||||
|
||||
// Initialize file server if static options provided
|
||||
if (this.options.static) {
|
||||
this.fileServer = new FileServer(this.options.static);
|
||||
}
|
||||
|
||||
// Initialize WebDAV handler if configured
|
||||
if (this.options.webdav) {
|
||||
this.webdavHandler = new WebDAVHandler(this.options.webdav);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a controller class or instance
|
||||
*/
|
||||
register(controllerOrInstance: Function | object): this {
|
||||
if (typeof controllerOrInstance === 'function') {
|
||||
// It's a class constructor
|
||||
const instance = new (controllerOrInstance as new () => any)();
|
||||
ControllerRegistry.registerInstance(instance);
|
||||
} else {
|
||||
// It's an instance
|
||||
ControllerRegistry.registerInstance(controllerOrInstance);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom request handler (bypasses decorator routing)
|
||||
*/
|
||||
setHandler(handler: TRequestHandler): this {
|
||||
this.customHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
async start(): Promise<ISmartServeInstance> {
|
||||
if (this.instance) {
|
||||
throw new ServerAlreadyRunningError();
|
||||
}
|
||||
|
||||
// Create adapter for current runtime
|
||||
this.adapter = await AdapterFactory.createAdapter(this.options);
|
||||
|
||||
// Create request handler
|
||||
const handler = this.createRequestHandler();
|
||||
|
||||
// Start server
|
||||
this.instance = await this.adapter.start(handler);
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (this.adapter) {
|
||||
await this.adapter.stop();
|
||||
this.adapter = null;
|
||||
this.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server instance (if running)
|
||||
*/
|
||||
getInstance(): ISmartServeInstance | null {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if server is running
|
||||
*/
|
||||
isRunning(): boolean {
|
||||
return this.instance !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the main request handler
|
||||
*/
|
||||
private createRequestHandler(): TRequestHandler {
|
||||
return async (request: Request, connectionInfo: IConnectionInfo): Promise<Response> => {
|
||||
// Use custom handler if set
|
||||
if (this.customHandler) {
|
||||
return this.customHandler(request, connectionInfo);
|
||||
}
|
||||
|
||||
// Parse URL and method
|
||||
const url = new URL(request.url);
|
||||
const method = request.method.toUpperCase() as THttpMethod;
|
||||
|
||||
// Handle WebDAV requests first if handler is configured
|
||||
if (this.webdavHandler && this.webdavHandler.isWebDAVRequest(request)) {
|
||||
try {
|
||||
return await this.webdavHandler.handle(request);
|
||||
} catch (error) {
|
||||
return this.handleError(error as Error, request);
|
||||
}
|
||||
}
|
||||
|
||||
// Match route first
|
||||
const match = ControllerRegistry.matchRoute(url.pathname, method);
|
||||
|
||||
if (!match) {
|
||||
// No route found, try WebDAV for GET/PUT/DELETE/HEAD (standard HTTP methods WebDAV also handles)
|
||||
if (this.webdavHandler) {
|
||||
try {
|
||||
return await this.webdavHandler.handle(request);
|
||||
} catch (error) {
|
||||
return this.handleError(error as Error, request);
|
||||
}
|
||||
}
|
||||
|
||||
// Try static files
|
||||
if (this.fileServer && (method === 'GET' || method === 'HEAD')) {
|
||||
try {
|
||||
const staticResponse = await this.fileServer.serve(request);
|
||||
if (staticResponse) {
|
||||
return staticResponse;
|
||||
}
|
||||
} catch (error) {
|
||||
return this.handleError(error as Error, request);
|
||||
}
|
||||
}
|
||||
|
||||
// Still no match, return 404
|
||||
const error = new RouteNotFoundError(url.pathname, method);
|
||||
if (this.options.onError) {
|
||||
return this.options.onError(error, request);
|
||||
}
|
||||
return error.toResponse();
|
||||
}
|
||||
|
||||
const { route, params } = match;
|
||||
|
||||
try {
|
||||
// Create request context
|
||||
const context = await this.createContext(request, url, params, connectionInfo);
|
||||
|
||||
// Run interceptors and handler
|
||||
return await this.executeRoute(route, context);
|
||||
} catch (error) {
|
||||
return this.handleError(error as Error, request);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create request context from Request object
|
||||
*/
|
||||
private async createContext(
|
||||
request: Request,
|
||||
url: URL,
|
||||
params: Record<string, string>,
|
||||
connectionInfo: IConnectionInfo
|
||||
): Promise<IRequestContext> {
|
||||
// Parse query params
|
||||
const query: Record<string, string> = {};
|
||||
url.searchParams.forEach((value, key) => {
|
||||
query[key] = value;
|
||||
});
|
||||
|
||||
// Parse body (lazy)
|
||||
let body: any = undefined;
|
||||
const contentType = request.headers.get('content-type');
|
||||
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
if (contentType?.includes('application/json')) {
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
||||
try {
|
||||
const text = await request.text();
|
||||
body = Object.fromEntries(new URLSearchParams(text));
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
} else if (contentType?.includes('text/')) {
|
||||
try {
|
||||
body = await request.text();
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
request,
|
||||
body,
|
||||
params,
|
||||
query,
|
||||
headers: request.headers,
|
||||
path: url.pathname,
|
||||
method: request.method.toUpperCase() as THttpMethod,
|
||||
url,
|
||||
runtime: this.adapter?.name ?? 'node',
|
||||
state: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute route with interceptor chain
|
||||
*/
|
||||
private async executeRoute(
|
||||
route: ICompiledRoute,
|
||||
context: IRequestContext
|
||||
): Promise<Response> {
|
||||
// Collect all request interceptors
|
||||
const requestInterceptors: TRequestInterceptor[] = [];
|
||||
const responseInterceptors: TResponseInterceptor[] = [];
|
||||
|
||||
for (const interceptor of route.interceptors) {
|
||||
if (interceptor.request) {
|
||||
const reqs = Array.isArray(interceptor.request)
|
||||
? interceptor.request
|
||||
: [interceptor.request];
|
||||
requestInterceptors.push(...reqs);
|
||||
}
|
||||
if (interceptor.response) {
|
||||
const ress = Array.isArray(interceptor.response)
|
||||
? interceptor.response
|
||||
: [interceptor.response];
|
||||
responseInterceptors.push(...ress);
|
||||
}
|
||||
}
|
||||
|
||||
// Run request interceptors
|
||||
let currentContext = context;
|
||||
for (const interceptor of requestInterceptors) {
|
||||
const result = await interceptor(currentContext);
|
||||
|
||||
if (result instanceof Response) {
|
||||
// Short-circuit with response
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result && typeof result === 'object' && 'request' in result) {
|
||||
// Updated context
|
||||
currentContext = result as IRequestContext;
|
||||
}
|
||||
// undefined means continue with current context
|
||||
}
|
||||
|
||||
// Execute handler
|
||||
let handlerResult = await route.handler(currentContext);
|
||||
|
||||
// Run response interceptors (in reverse order for onion model)
|
||||
for (let i = responseInterceptors.length - 1; i >= 0; i--) {
|
||||
const interceptor = responseInterceptors[i];
|
||||
const result = await interceptor(handlerResult, currentContext);
|
||||
|
||||
if (result instanceof Response) {
|
||||
return result;
|
||||
}
|
||||
|
||||
handlerResult = result;
|
||||
}
|
||||
|
||||
// Convert result to Response
|
||||
return this.resultToResponse(handlerResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert handler result to Response
|
||||
*/
|
||||
private resultToResponse(result: any): Response {
|
||||
// Already a Response
|
||||
if (result instanceof Response) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Null/undefined
|
||||
if (result === null || result === undefined) {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
// String
|
||||
if (typeof result === 'string') {
|
||||
return new Response(result, {
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
|
||||
// Object/Array - serialize as JSON
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors
|
||||
*/
|
||||
private handleError(error: Error, request: Request): Response {
|
||||
// Custom error handler
|
||||
if (this.options.onError) {
|
||||
try {
|
||||
const result = this.options.onError(error, request);
|
||||
if (result instanceof Promise) {
|
||||
// Can't await here, return 500
|
||||
console.error('Error in error handler:', error);
|
||||
return new Response('Internal Server Error', { status: 500 });
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
// Error in error handler
|
||||
}
|
||||
}
|
||||
|
||||
// HttpError
|
||||
if (error instanceof HttpError) {
|
||||
return error.toResponse();
|
||||
}
|
||||
|
||||
// Unknown error
|
||||
console.error('Unhandled error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Internal Server Error' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
129
ts/core/smartserve.errors.ts
Normal file
129
ts/core/smartserve.errors.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Custom error classes for @push.rocks/smartserve
|
||||
*/
|
||||
|
||||
/**
|
||||
* HTTP error with status code
|
||||
* Thrown from handlers to return specific HTTP responses
|
||||
*/
|
||||
export class HttpError extends Error {
|
||||
public readonly status: number;
|
||||
public readonly details?: Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
message: string,
|
||||
details?: Record<string, unknown>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'HttpError';
|
||||
this.status = status;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to HTTP Response
|
||||
*/
|
||||
toResponse(): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: this.message,
|
||||
status: this.status,
|
||||
...(this.details ? { details: this.details } : {}),
|
||||
}),
|
||||
{
|
||||
status: this.status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Common HTTP errors as static factory methods
|
||||
static badRequest(message = 'Bad Request', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(400, message, details);
|
||||
}
|
||||
|
||||
static unauthorized(message = 'Unauthorized', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(401, message, details);
|
||||
}
|
||||
|
||||
static forbidden(message = 'Forbidden', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(403, message, details);
|
||||
}
|
||||
|
||||
static notFound(message = 'Not Found', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(404, message, details);
|
||||
}
|
||||
|
||||
static methodNotAllowed(message = 'Method Not Allowed', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(405, message, details);
|
||||
}
|
||||
|
||||
static conflict(message = 'Conflict', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(409, message, details);
|
||||
}
|
||||
|
||||
static unprocessableEntity(message = 'Unprocessable Entity', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(422, message, details);
|
||||
}
|
||||
|
||||
static tooManyRequests(message = 'Too Many Requests', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(429, message, details);
|
||||
}
|
||||
|
||||
static internalServerError(message = 'Internal Server Error', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(500, message, details);
|
||||
}
|
||||
|
||||
static notImplemented(message = 'Not Implemented', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(501, message, details);
|
||||
}
|
||||
|
||||
static badGateway(message = 'Bad Gateway', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(502, message, details);
|
||||
}
|
||||
|
||||
static serviceUnavailable(message = 'Service Unavailable', details?: Record<string, unknown>): HttpError {
|
||||
return new HttpError(503, message, details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when route is not found
|
||||
*/
|
||||
export class RouteNotFoundError extends HttpError {
|
||||
constructor(path: string, method: string) {
|
||||
super(404, `Route not found: ${method} ${path}`, { path, method });
|
||||
this.name = 'RouteNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when adapter is not supported
|
||||
*/
|
||||
export class UnsupportedRuntimeError extends Error {
|
||||
constructor(runtime: string) {
|
||||
super(`Unsupported runtime: ${runtime}`);
|
||||
this.name = 'UnsupportedRuntimeError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when server is already running
|
||||
*/
|
||||
export class ServerAlreadyRunningError extends Error {
|
||||
constructor() {
|
||||
super('Server is already running');
|
||||
this.name = 'ServerAlreadyRunningError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when server is not running
|
||||
*/
|
||||
export class ServerNotRunningError extends Error {
|
||||
constructor() {
|
||||
super('Server is not running');
|
||||
this.name = 'ServerNotRunningError';
|
||||
}
|
||||
}
|
||||
348
ts/core/smartserve.interfaces.ts
Normal file
348
ts/core/smartserve.interfaces.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Core interfaces for @push.rocks/smartserve
|
||||
* Uses Web Standards API (Request/Response) for cross-platform compatibility
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// HTTP Types
|
||||
// =============================================================================
|
||||
|
||||
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';
|
||||
|
||||
export type TRuntime = 'node' | 'deno' | 'bun';
|
||||
|
||||
// =============================================================================
|
||||
// Request Context
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Request context passed to handlers and interceptors
|
||||
* Wraps Web Standard Request with additional utilities
|
||||
*/
|
||||
export interface IRequestContext<TBody = unknown> {
|
||||
/** Original Web Standards Request */
|
||||
readonly request: Request;
|
||||
/** Parsed request body (typed) */
|
||||
readonly body: TBody;
|
||||
/** URL path parameters extracted from route */
|
||||
readonly params: Record<string, string>;
|
||||
/** URL query parameters */
|
||||
readonly query: Record<string, string>;
|
||||
/** Request headers accessor */
|
||||
readonly headers: Headers;
|
||||
/** Matched route path pattern */
|
||||
readonly path: string;
|
||||
/** HTTP method */
|
||||
readonly method: THttpMethod;
|
||||
/** Full URL object */
|
||||
readonly url: URL;
|
||||
/** Runtime environment */
|
||||
readonly runtime: TRuntime;
|
||||
/** Route-specific state bag for passing data between interceptors */
|
||||
state: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Interceptor Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Request interceptor - runs BEFORE the handler
|
||||
* Can:
|
||||
* - Return modified context to continue
|
||||
* - Return Response to short-circuit
|
||||
* - Return void/undefined to continue with original context
|
||||
* - Throw to trigger error handling
|
||||
*/
|
||||
export type TRequestInterceptor<TBody = unknown> = (
|
||||
ctx: IRequestContext<TBody>
|
||||
) => Promise<IRequestContext<TBody> | Response | void> | IRequestContext<TBody> | Response | void;
|
||||
|
||||
/**
|
||||
* Response interceptor - runs AFTER the handler
|
||||
* Can:
|
||||
* - Return modified response data
|
||||
* - Return a Response object directly
|
||||
*/
|
||||
export type TResponseInterceptor<TRes = unknown> = (
|
||||
response: TRes,
|
||||
ctx: IRequestContext
|
||||
) => Promise<TRes | Response> | TRes | Response;
|
||||
|
||||
/**
|
||||
* Guard function - simplified boolean check for authorization
|
||||
* Returns true to allow, false to reject with 403
|
||||
*/
|
||||
export type TGuardFunction<TBody = unknown> = (
|
||||
ctx: IRequestContext<TBody>
|
||||
) => Promise<boolean> | boolean;
|
||||
|
||||
/**
|
||||
* Combined interceptor options for @Intercept decorator
|
||||
*/
|
||||
export interface IInterceptOptions<TBody = unknown, TRes = unknown> {
|
||||
/** Request interceptors (run before handler) */
|
||||
request?: TRequestInterceptor<TBody> | TRequestInterceptor<TBody>[];
|
||||
/** Response interceptors (run after handler) */
|
||||
response?: TResponseInterceptor<TRes> | TResponseInterceptor<TRes>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for @Guard decorator
|
||||
*/
|
||||
export interface IGuardOptions {
|
||||
/** Custom response when guard rejects (default: 403 Forbidden) */
|
||||
onReject?: (ctx: IRequestContext) => Response | Promise<Response>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Route Handler Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Route handler function signature
|
||||
*/
|
||||
export type TRouteHandler<TReq = unknown, TRes = unknown> = (
|
||||
ctx: IRequestContext<TReq>
|
||||
) => Promise<TRes> | TRes;
|
||||
|
||||
/**
|
||||
* Options for method decorators (@Get, @Post, etc.)
|
||||
*/
|
||||
export interface IMethodOptions {
|
||||
/** Path segment (appended to class route) */
|
||||
path?: string;
|
||||
/** Content-Type for response */
|
||||
contentType?: string;
|
||||
/** HTTP status code for successful response */
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for @Route class decorator
|
||||
*/
|
||||
export interface IRouteOptions {
|
||||
/** Base path for all routes in this controller */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WebSocket Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* WebSocket message types
|
||||
*/
|
||||
export interface IWebSocketMessage {
|
||||
type: 'text' | 'binary';
|
||||
text?: string;
|
||||
data?: Uint8Array;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket peer connection
|
||||
*/
|
||||
export interface IWebSocketPeer {
|
||||
/** Unique connection ID */
|
||||
id: string;
|
||||
/** Connection URL */
|
||||
url: string;
|
||||
/** WebSocket ready state */
|
||||
readyState: 0 | 1 | 2 | 3;
|
||||
/** Negotiated subprotocol */
|
||||
protocol: string;
|
||||
/** Negotiated extensions */
|
||||
extensions: string;
|
||||
/** Send text message */
|
||||
send(data: string): void;
|
||||
/** Send binary message */
|
||||
sendBinary(data: Uint8Array | ArrayBuffer): void;
|
||||
/** Close connection */
|
||||
close(code?: number, reason?: string): void;
|
||||
/** Send ping */
|
||||
ping(data?: Uint8Array): void;
|
||||
/** Force close without handshake */
|
||||
terminate(): void;
|
||||
/** Request context from upgrade */
|
||||
context: IRequestContext;
|
||||
/** Custom per-peer data storage */
|
||||
data: Map<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket event hooks
|
||||
*/
|
||||
export interface IWebSocketHooks {
|
||||
onOpen?: (peer: IWebSocketPeer) => void | Promise<void>;
|
||||
onMessage?: (peer: IWebSocketPeer, message: IWebSocketMessage) => void | Promise<void>;
|
||||
onClose?: (peer: IWebSocketPeer, code: number, reason: string) => void | Promise<void>;
|
||||
onError?: (peer: IWebSocketPeer, error: Error) => void | Promise<void>;
|
||||
onPing?: (peer: IWebSocketPeer, data: Uint8Array) => void | Promise<void>;
|
||||
onPong?: (peer: IWebSocketPeer, data: Uint8Array) => void | Promise<void>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Server Configuration
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* TLS/SSL configuration
|
||||
*/
|
||||
export interface ITLSConfig {
|
||||
/** Certificate (PEM format) */
|
||||
cert: string | Uint8Array;
|
||||
/** Private key (PEM format) */
|
||||
key: string | Uint8Array;
|
||||
/** CA chain (PEM format) */
|
||||
ca?: string | Uint8Array;
|
||||
/** ALPN protocols */
|
||||
alpnProtocols?: string[];
|
||||
/** Minimum TLS version */
|
||||
minVersion?: 'TLSv1.2' | 'TLSv1.3';
|
||||
/** Passphrase for encrypted key */
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep-alive configuration
|
||||
*/
|
||||
export interface IKeepAliveConfig {
|
||||
enabled: boolean;
|
||||
timeout?: number;
|
||||
maxRequests?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static file serving options
|
||||
*/
|
||||
export interface IStaticOptions {
|
||||
/** Root directory path */
|
||||
root: string;
|
||||
/** Index files to look for */
|
||||
index?: string[];
|
||||
/** How to handle dotfiles */
|
||||
dotFiles?: 'allow' | 'deny' | 'ignore';
|
||||
/** Generate ETags */
|
||||
etag?: boolean;
|
||||
/** Add Last-Modified header */
|
||||
lastModified?: boolean;
|
||||
/** Cache-Control header value or function */
|
||||
cacheControl?: string | ((path: string) => string);
|
||||
/** File extensions to try */
|
||||
extensions?: string[];
|
||||
/** Enable directory listing */
|
||||
directoryListing?: boolean | IDirectoryListingOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory listing options
|
||||
*/
|
||||
export interface IDirectoryListingOptions {
|
||||
/** Custom template function */
|
||||
template?: (files: IFileEntry[]) => string | Response;
|
||||
/** Show hidden files */
|
||||
showHidden?: boolean;
|
||||
/** Sort field */
|
||||
sortBy?: 'name' | 'size' | 'modified';
|
||||
/** Sort order */
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
/**
|
||||
* File entry for directory listing
|
||||
*/
|
||||
export interface IFileEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
size: number;
|
||||
modified: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebDAV configuration
|
||||
*/
|
||||
export interface IWebDAVConfig {
|
||||
/** Root directory path */
|
||||
root: string;
|
||||
/** Authentication handler */
|
||||
auth?: (ctx: IRequestContext) => boolean | Promise<boolean>;
|
||||
/** Enable locking */
|
||||
locking?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main server configuration
|
||||
*/
|
||||
export interface ISmartServeOptions {
|
||||
/** Port to listen on */
|
||||
port: number;
|
||||
/** Hostname to bind to */
|
||||
hostname?: string;
|
||||
/** TLS configuration for HTTPS */
|
||||
tls?: ITLSConfig;
|
||||
/** WebSocket configuration */
|
||||
websocket?: IWebSocketHooks;
|
||||
/** Static file serving */
|
||||
static?: IStaticOptions | string;
|
||||
/** WebDAV configuration */
|
||||
webdav?: IWebDAVConfig;
|
||||
/** Connection timeout (ms) */
|
||||
connectionTimeout?: number;
|
||||
/** Keep-alive settings */
|
||||
keepAlive?: IKeepAliveConfig;
|
||||
/** Global error handler */
|
||||
onError?: (error: Error, request?: Request) => Response | Promise<Response>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Server Instance
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Server statistics
|
||||
*/
|
||||
export interface IServerStats {
|
||||
uptime: number;
|
||||
requestsTotal: number;
|
||||
requestsActive: number;
|
||||
connectionsTotal: number;
|
||||
connectionsActive: number;
|
||||
bytesReceived: number;
|
||||
bytesSent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Running server instance
|
||||
*/
|
||||
export interface ISmartServeInstance {
|
||||
/** Listening port */
|
||||
port: number;
|
||||
/** Bound hostname */
|
||||
hostname: string;
|
||||
/** Is HTTPS enabled */
|
||||
secure: boolean;
|
||||
/** Runtime environment */
|
||||
runtime: TRuntime;
|
||||
/** Stop the server */
|
||||
stop(): Promise<void>;
|
||||
/** Get server statistics */
|
||||
stats(): IServerStats;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Connection Info
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Connection information
|
||||
*/
|
||||
export interface IConnectionInfo {
|
||||
remoteAddr: string;
|
||||
remotePort: number;
|
||||
localAddr: string;
|
||||
localPort: number;
|
||||
encrypted: boolean;
|
||||
tlsVersion?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user