import * as plugins from '../plugins.js'; import type { ISmartServeOptions, ISmartServeInstance, IRequestContext, IConnectionInfo, IServerStats, IWebSocketHeartbeatOptions, TRuntime, THttpMethod, } from '../core/smartserve.interfaces.js'; interface IResolvedWebSocketHeartbeatOptions { intervalMs: number; timeoutMs: number; payload?: Uint8Array; } /** * Adapter characteristics - what each runtime supports */ export interface IAdapterCharacteristics { /** Zero-copy streaming support */ zeroCopyStreaming: boolean; /** HTTP/2 support */ http2Support: boolean; /** Maximum concurrent connections */ maxConnections: number | 'unlimited'; /** Native WebSocket upgrade support */ nativeWebSocket: boolean; } /** * Handler function that receives web standard Request and returns Response */ export type TRequestHandler = ( request: Request, info: IConnectionInfo ) => Response | Promise; /** * Abstract base adapter for all runtime implementations */ export abstract class BaseAdapter { protected options: ISmartServeOptions; protected handler: TRequestHandler | null = null; protected stats: IServerStats = { uptime: 0, requestsTotal: 0, requestsActive: 0, connectionsTotal: 0, connectionsActive: 0, bytesReceived: 0, bytesSent: 0, }; protected startTime: number = 0; constructor(options: ISmartServeOptions) { this.options = options; } /** * Runtime name */ abstract get name(): TRuntime; /** * Adapter characteristics */ abstract get characteristics(): IAdapterCharacteristics; /** * Check if this adapter is supported in current runtime */ abstract isSupported(): boolean; /** * Start the server */ abstract start(handler: TRequestHandler): Promise; /** * Stop the server */ abstract stop(): Promise; /** * Get current server statistics */ getStats(): IServerStats { return { ...this.stats, uptime: this.startTime > 0 ? Date.now() - this.startTime : 0, }; } /** * Create ISmartServeInstance from adapter */ protected createInstance(): ISmartServeInstance { return { port: this.options.port, hostname: this.options.hostname ?? '0.0.0.0', secure: !!this.options.tls, runtime: this.name, stop: () => this.stop(), stats: () => this.getStats(), }; } /** * Parse URL from request for cross-platform compatibility */ protected parseUrl(request: Request): URL { return new URL(request.url); } /** * Parse query parameters from URL */ protected parseQuery(url: URL): Record { const query: Record = {}; url.searchParams.forEach((value, key) => { query[key] = value; }); return query; } /** * Get HTTP method from request */ protected parseMethod(request: Request): THttpMethod { const method = request.method.toUpperCase(); if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(method)) { return method as THttpMethod; } return 'GET'; } protected resolveWebSocketHeartbeatOptions( defaultEnabled: boolean, ): IResolvedWebSocketHeartbeatOptions | null { const heartbeat = this.options.websocket?.heartbeat; const heartbeatOptions: IWebSocketHeartbeatOptions = typeof heartbeat === 'object' && heartbeat !== null ? heartbeat : {}; const enabled = typeof heartbeat === 'boolean' ? heartbeat : heartbeatOptions.enabled ?? defaultEnabled; if (!enabled) { return null; } const intervalMs = heartbeatOptions.intervalMs ?? 30_000; const timeoutMs = heartbeatOptions.timeoutMs ?? 15_000; this.assertPositiveFiniteNumber(intervalMs, 'websocket.heartbeat.intervalMs'); this.assertPositiveFiniteNumber(timeoutMs, 'websocket.heartbeat.timeoutMs'); if (timeoutMs >= intervalMs) { throw new Error('websocket.heartbeat.timeoutMs must be lower than websocket.heartbeat.intervalMs'); } return { intervalMs, timeoutMs, payload: this.normalizeWebSocketHeartbeatPayload(heartbeatOptions.payload), }; } private assertPositiveFiniteNumber(value: number, optionName: string): void { if (!Number.isFinite(value) || value <= 0) { throw new Error(`${optionName} must be a positive finite number`); } } private normalizeWebSocketHeartbeatPayload(payload?: string | Uint8Array): Uint8Array | undefined { if (payload === undefined) { return undefined; } if (typeof payload === 'string') { return new TextEncoder().encode(payload); } return payload; } }