This commit is contained in:
2025-11-29 15:24:00 +00:00
commit 9411b5ee49
42 changed files with 14742 additions and 0 deletions

132
ts/adapters/adapter.base.ts Normal file
View File

@@ -0,0 +1,132 @@
import * as plugins from '../plugins.js';
import type {
ISmartServeOptions,
ISmartServeInstance,
IRequestContext,
IConnectionInfo,
IServerStats,
TRuntime,
THttpMethod,
} from '../core/smartserve.interfaces.js';
/**
* 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<Response>;
/**
* 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<ISmartServeInstance>;
/**
* Stop the server
*/
abstract stop(): Promise<void>;
/**
* 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<string, string> {
const query: Record<string, string> = {};
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';
}
}

161
ts/adapters/adapter.bun.ts Normal file
View File

@@ -0,0 +1,161 @@
import type { ISmartServeInstance, IConnectionInfo } from '../core/smartserve.interfaces.js';
import { BaseAdapter, type IAdapterCharacteristics, type TRequestHandler } from './adapter.base.js';
// Bun types (for type checking without requiring Bun runtime)
declare const Bun: {
serve(options: any): { stop(): void; port: number; hostname: string };
file(path: string): any;
};
/**
* Bun adapter - zero overhead, native Request/Response
*/
export class BunAdapter extends BaseAdapter {
private server: { stop(): void; port: number; hostname: string } | null = null;
get name(): 'bun' {
return 'bun';
}
get characteristics(): IAdapterCharacteristics {
return {
zeroCopyStreaming: true,
http2Support: false, // Bun currently HTTP/1.1 only
maxConnections: 'unlimited',
nativeWebSocket: true,
};
}
isSupported(): boolean {
return typeof (globalThis as any).Bun !== 'undefined';
}
async start(handler: TRequestHandler): Promise<ISmartServeInstance> {
if (!this.isSupported()) {
throw new Error('Bun runtime is not available');
}
this.handler = handler;
this.startTime = Date.now();
const serveOptions: any = {
port: this.options.port,
hostname: this.options.hostname ?? '0.0.0.0',
fetch: async (request: Request, server: any) => {
this.stats.requestsTotal++;
this.stats.requestsActive++;
try {
// Handle WebSocket upgrade
if (this.options.websocket && request.headers.get('upgrade') === 'websocket') {
const upgraded = server.upgrade(request);
if (upgraded) {
return undefined; // Bun handles the upgrade
}
}
// Create connection info
const connectionInfo: IConnectionInfo = {
remoteAddr: server.requestIP(request)?.address ?? 'unknown',
remotePort: 0,
localAddr: this.options.hostname ?? '0.0.0.0',
localPort: this.options.port,
encrypted: !!this.options.tls,
};
return await handler(request, connectionInfo);
} catch (error) {
if (this.options.onError) {
return this.options.onError(error as Error, request);
}
return new Response('Internal Server Error', { status: 500 });
} finally {
this.stats.requestsActive--;
}
},
};
// Add TLS if configured
if (this.options.tls) {
serveOptions.tls = {
cert: typeof this.options.tls.cert === 'string'
? Bun.file(this.options.tls.cert)
: this.options.tls.cert,
key: typeof this.options.tls.key === 'string'
? Bun.file(this.options.tls.key)
: this.options.tls.key,
passphrase: this.options.tls.passphrase,
};
}
// Add WebSocket handlers if configured
if (this.options.websocket) {
serveOptions.websocket = this.createWebSocketHandler();
}
this.server = Bun.serve(serveOptions);
return this.createInstance();
}
async stop(): Promise<void> {
if (this.server) {
this.server.stop();
this.server = null;
}
}
private createWebSocketHandler(): any {
const hooks = this.options.websocket;
if (!hooks) return undefined;
return {
open: (ws: any) => {
const peer = this.wrapBunWebSocket(ws);
hooks.onOpen?.(peer);
},
message: (ws: any, message: string | ArrayBuffer) => {
const peer = this.wrapBunWebSocket(ws);
const msg = {
type: typeof message === 'string' ? 'text' as const : 'binary' as const,
text: typeof message === 'string' ? message : undefined,
data: message instanceof ArrayBuffer ? new Uint8Array(message) : undefined,
size: typeof message === 'string' ? message.length : (message as ArrayBuffer).byteLength,
};
hooks.onMessage?.(peer, msg);
},
close: (ws: any, code: number, reason: string) => {
const peer = this.wrapBunWebSocket(ws);
hooks.onClose?.(peer, code, reason);
},
error: (ws: any, error: Error) => {
const peer = this.wrapBunWebSocket(ws);
hooks.onError?.(peer, error);
},
ping: (ws: any, data: ArrayBuffer) => {
const peer = this.wrapBunWebSocket(ws);
hooks.onPing?.(peer, new Uint8Array(data));
},
pong: (ws: any, data: ArrayBuffer) => {
const peer = this.wrapBunWebSocket(ws);
hooks.onPong?.(peer, new Uint8Array(data));
},
};
}
private wrapBunWebSocket(ws: any): any {
return {
id: ws.data?.id ?? crypto.randomUUID(),
url: ws.data?.url ?? '',
get readyState() { return ws.readyState; },
protocol: ws.protocol ?? '',
extensions: ws.extensions ?? '',
send: (data: string) => ws.send(data),
sendBinary: (data: Uint8Array | ArrayBuffer) => ws.send(data),
close: (code?: number, reason?: string) => ws.close(code, reason),
ping: (data?: Uint8Array) => ws.ping(data),
terminate: () => ws.terminate(),
context: {} as any,
data: ws.data?.customData ?? new Map(),
};
}
}

141
ts/adapters/adapter.deno.ts Normal file
View File

@@ -0,0 +1,141 @@
import type { ISmartServeInstance, IConnectionInfo } from '../core/smartserve.interfaces.js';
import { BaseAdapter, type IAdapterCharacteristics, type TRequestHandler } from './adapter.base.js';
// Deno types (for type checking without requiring Deno runtime)
declare const Deno: {
serve(options: any, handler?: any): { shutdown(): Promise<void>; finished: Promise<void> };
upgradeWebSocket(request: Request): { socket: WebSocket; response: Response };
};
/**
* Deno adapter - zero overhead, native Request/Response
*/
export class DenoAdapter extends BaseAdapter {
private server: { shutdown(): Promise<void>; finished: Promise<void> } | null = null;
get name(): 'deno' {
return 'deno';
}
get characteristics(): IAdapterCharacteristics {
return {
zeroCopyStreaming: true,
http2Support: true,
maxConnections: 'unlimited',
nativeWebSocket: true,
};
}
isSupported(): boolean {
return typeof (globalThis as any).Deno !== 'undefined';
}
async start(handler: TRequestHandler): Promise<ISmartServeInstance> {
if (!this.isSupported()) {
throw new Error('Deno runtime is not available');
}
this.handler = handler;
this.startTime = Date.now();
const serveOptions: any = {
port: this.options.port,
hostname: this.options.hostname ?? '0.0.0.0',
};
// Add TLS if configured
if (this.options.tls) {
serveOptions.cert = this.options.tls.cert;
serveOptions.key = this.options.tls.key;
}
this.server = Deno.serve(serveOptions, async (request: Request, info: any) => {
this.stats.requestsTotal++;
this.stats.requestsActive++;
try {
// Handle WebSocket upgrade
if (this.options.websocket && request.headers.get('upgrade') === 'websocket') {
const { socket, response } = Deno.upgradeWebSocket(request);
this.attachWebSocketHooks(socket, request);
return response;
}
// Create connection info
const connectionInfo: IConnectionInfo = {
remoteAddr: info?.remoteAddr?.hostname ?? 'unknown',
remotePort: info?.remoteAddr?.port ?? 0,
localAddr: this.options.hostname ?? '0.0.0.0',
localPort: this.options.port,
encrypted: !!this.options.tls,
};
return await handler(request, connectionInfo);
} catch (error) {
if (this.options.onError) {
return this.options.onError(error as Error, request);
}
return new Response('Internal Server Error', { status: 500 });
} finally {
this.stats.requestsActive--;
}
});
return this.createInstance();
}
async stop(): Promise<void> {
if (this.server) {
await this.server.shutdown();
await this.server.finished;
this.server = null;
}
}
private attachWebSocketHooks(socket: WebSocket, request: Request): void {
const hooks = this.options.websocket;
if (!hooks) return;
const peer = this.createWebSocketPeer(socket, request);
socket.onopen = () => {
hooks.onOpen?.(peer);
};
socket.onmessage = (event) => {
const message = {
type: typeof event.data === 'string' ? 'text' as const : 'binary' as const,
text: typeof event.data === 'string' ? event.data : undefined,
data: event.data instanceof Uint8Array ? event.data : undefined,
size: typeof event.data === 'string' ? event.data.length : (event.data as ArrayBuffer).byteLength,
};
hooks.onMessage?.(peer, message);
};
socket.onclose = (event) => {
hooks.onClose?.(peer, event.code, event.reason);
};
socket.onerror = (event) => {
hooks.onError?.(peer, new Error('WebSocket error'));
};
}
private createWebSocketPeer(socket: WebSocket, request: Request): any {
const url = this.parseUrl(request);
return {
id: crypto.randomUUID(),
url: url.pathname,
get readyState() { return socket.readyState; },
protocol: socket.protocol,
extensions: socket.extensions,
send: (data: string) => socket.send(data),
sendBinary: (data: Uint8Array | ArrayBuffer) => socket.send(data),
close: (code?: number, reason?: string) => socket.close(code, reason),
ping: () => { /* Deno handles ping/pong automatically */ },
terminate: () => socket.close(),
context: {} as any, // Will be populated with IRequestContext
data: new Map(),
};
}
}

View File

@@ -0,0 +1,77 @@
import * as plugins from '../plugins.js';
import type { ISmartServeOptions, TRuntime } from '../core/smartserve.interfaces.js';
import { UnsupportedRuntimeError } from '../core/smartserve.errors.js';
import type { BaseAdapter } from './adapter.base.js';
/**
* Factory for creating runtime-specific adapters
* Uses @push.rocks/smartenv for runtime detection
*/
export class AdapterFactory {
private static smartenv = new plugins.smartenv.Smartenv();
/**
* Detect current runtime
*/
static detectRuntime(): TRuntime {
if (this.smartenv.isBrowser) {
throw new UnsupportedRuntimeError('browser');
}
// Check for Deno
if (typeof (globalThis as any).Deno !== 'undefined') {
return 'deno';
}
// Check for Bun
if (typeof (globalThis as any).Bun !== 'undefined') {
return 'bun';
}
// Default to Node.js
return 'node';
}
/**
* Create an adapter for the current runtime
*/
static async createAdapter(options: ISmartServeOptions): Promise<BaseAdapter> {
const runtime = this.detectRuntime();
switch (runtime) {
case 'deno': {
const { DenoAdapter } = await import('./adapter.deno.js');
return new DenoAdapter(options);
}
case 'bun': {
const { BunAdapter } = await import('./adapter.bun.js');
return new BunAdapter(options);
}
case 'node': {
const { NodeAdapter } = await import('./adapter.node.js');
return new NodeAdapter(options);
}
default:
throw new UnsupportedRuntimeError(runtime);
}
}
/**
* Check if a specific runtime is available
*/
static isRuntimeAvailable(runtime: TRuntime): boolean {
switch (runtime) {
case 'deno':
return typeof (globalThis as any).Deno !== 'undefined';
case 'bun':
return typeof (globalThis as any).Bun !== 'undefined';
case 'node':
return typeof process !== 'undefined' && !!process.versions?.node;
default:
return false;
}
}
}

317
ts/adapters/adapter.node.ts Normal file
View File

@@ -0,0 +1,317 @@
import * as plugins from '../plugins.js';
import type { ISmartServeInstance, IConnectionInfo } from '../core/smartserve.interfaces.js';
import { BaseAdapter, type IAdapterCharacteristics, type TRequestHandler } from './adapter.base.js';
/**
* Node.js adapter - converts IncomingMessage/ServerResponse to web standards
*/
export class NodeAdapter extends BaseAdapter {
private server: plugins.http.Server | plugins.https.Server | null = null;
get name(): 'node' {
return 'node';
}
get characteristics(): IAdapterCharacteristics {
return {
zeroCopyStreaming: false, // Requires conversion
http2Support: true,
maxConnections: 16384,
nativeWebSocket: false, // Requires ws library
};
}
isSupported(): boolean {
return typeof process !== 'undefined' && !!process.versions?.node;
}
async start(handler: TRequestHandler): Promise<ISmartServeInstance> {
this.handler = handler;
this.startTime = Date.now();
const requestListener = this.createRequestListener(handler);
if (this.options.tls) {
// Convert Uint8Array to Buffer if needed
const toBuffer = (data: string | Uint8Array | undefined): string | Buffer | undefined => {
if (data === undefined) return undefined;
if (typeof data === 'string') return data;
return Buffer.from(data);
};
const tlsOptions: plugins.https.ServerOptions = {
cert: toBuffer(this.options.tls.cert),
key: toBuffer(this.options.tls.key),
ca: toBuffer(this.options.tls.ca),
passphrase: this.options.tls.passphrase,
};
if (this.options.tls.alpnProtocols) {
tlsOptions.ALPNProtocols = this.options.tls.alpnProtocols;
}
if (this.options.tls.minVersion) {
tlsOptions.minVersion = this.options.tls.minVersion;
}
this.server = plugins.https.createServer(tlsOptions, requestListener);
} else {
this.server = plugins.http.createServer(requestListener);
}
// Configure keep-alive
if (this.options.keepAlive?.enabled) {
this.server.keepAliveTimeout = this.options.keepAlive.timeout ?? 5000;
(this.server as any).maxRequestsPerSocket = this.options.keepAlive.maxRequests ?? 1000;
}
// Set up connection tracking
this.server.on('connection', (socket) => {
this.stats.connectionsTotal++;
this.stats.connectionsActive++;
socket.on('close', () => {
this.stats.connectionsActive--;
});
});
// WebSocket upgrade handling (if ws library available)
if (this.options.websocket) {
await this.setupWebSocket();
}
return new Promise((resolve, reject) => {
this.server!.listen(
this.options.port,
this.options.hostname ?? '0.0.0.0',
() => {
resolve(this.createInstance());
}
);
this.server!.on('error', reject);
});
}
async stop(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.server) {
this.server.close((err) => {
if (err) reject(err);
else resolve();
});
this.server = null;
} else {
resolve();
}
});
}
/**
* Create Node.js request listener that converts to web standards
*/
private createRequestListener(handler: TRequestHandler) {
return async (
req: plugins.http.IncomingMessage,
res: plugins.http.ServerResponse
) => {
this.stats.requestsTotal++;
this.stats.requestsActive++;
try {
// Convert to web standard Request
const request = this.toWebRequest(req);
// Create connection info
const connectionInfo: IConnectionInfo = {
remoteAddr: req.socket.remoteAddress ?? 'unknown',
remotePort: req.socket.remotePort ?? 0,
localAddr: req.socket.localAddress ?? '0.0.0.0',
localPort: req.socket.localPort ?? this.options.port,
encrypted: !!(req.socket as any).encrypted,
tlsVersion: (req.socket as any).getCipher?.()?.version,
};
// Call handler and send response
const response = await handler(request, connectionInfo);
await this.sendResponse(res, response);
} catch (error) {
if (this.options.onError) {
try {
const errorResponse = await this.options.onError(error as Error);
await this.sendResponse(res, errorResponse);
} catch {
res.statusCode = 500;
res.end('Internal Server Error');
}
} else {
res.statusCode = 500;
res.end('Internal Server Error');
}
} finally {
this.stats.requestsActive--;
}
};
}
/**
* Convert Node.js IncomingMessage to Web Standard Request
*/
private toWebRequest(req: plugins.http.IncomingMessage): Request {
const protocol = (req.socket as any).encrypted ? 'https' : 'http';
const host = req.headers.host ?? 'localhost';
const url = new URL(req.url ?? '/', `${protocol}://${host}`);
// Convert headers
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (value !== undefined) {
if (Array.isArray(value)) {
value.forEach(v => headers.append(key, v));
} else {
headers.set(key, value);
}
}
}
// Create body stream for non-GET/HEAD requests
let body: ReadableStream<Uint8Array> | null = null;
const method = req.method?.toUpperCase() ?? 'GET';
if (method !== 'GET' && method !== 'HEAD') {
body = new ReadableStream({
start(controller) {
req.on('data', (chunk: Buffer) => {
controller.enqueue(new Uint8Array(chunk));
});
req.on('end', () => {
controller.close();
});
req.on('error', (err) => {
controller.error(err);
});
},
cancel() {
req.destroy();
},
});
}
// Use proper init object for Request
const init: RequestInit = {
method,
headers,
};
if (body) {
init.body = body;
// @ts-ignore - duplex is needed for streaming body in Node.js
init.duplex = 'half';
}
return new Request(url.toString(), init);
}
/**
* Send Web Standard Response via Node.js ServerResponse
*/
private async sendResponse(
res: plugins.http.ServerResponse,
response: Response
): Promise<void> {
res.statusCode = response.status;
res.statusMessage = response.statusText;
// Set headers
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
// Stream body
if (response.body) {
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
this.stats.bytesSent += value.byteLength;
}
} finally {
reader.releaseLock();
}
}
res.end();
}
/**
* Set up WebSocket support using ws library
*/
private async setupWebSocket(): Promise<void> {
const hooks = this.options.websocket;
if (!hooks || !this.server) return;
try {
// Dynamic import of ws library
const { WebSocketServer } = await import('ws');
const wss = new WebSocketServer({ noServer: true });
this.server.on('upgrade', (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
wss.on('connection', (ws: any, request: any) => {
const peer = this.wrapNodeWebSocket(ws, request);
hooks.onOpen?.(peer);
ws.on('message', (data: Buffer | string) => {
const message = {
type: typeof data === 'string' ? 'text' as const : 'binary' as const,
text: typeof data === 'string' ? data : undefined,
data: Buffer.isBuffer(data) ? new Uint8Array(data) : undefined,
size: typeof data === 'string' ? data.length : data.length,
};
hooks.onMessage?.(peer, message);
});
ws.on('close', (code: number, reason: Buffer) => {
hooks.onClose?.(peer, code, reason.toString());
});
ws.on('error', (error: Error) => {
hooks.onError?.(peer, error);
});
ws.on('ping', (data: Buffer) => {
hooks.onPing?.(peer, new Uint8Array(data));
});
ws.on('pong', (data: Buffer) => {
hooks.onPong?.(peer, new Uint8Array(data));
});
});
} catch {
console.warn('WebSocket support requires the "ws" package. Install with: pnpm add ws');
}
}
private wrapNodeWebSocket(ws: any, request: any): any {
return {
id: crypto.randomUUID(),
url: request.url ?? '',
get readyState() { return ws.readyState; },
protocol: ws.protocol ?? '',
extensions: ws.extensions ?? '',
send: (data: string) => ws.send(data),
sendBinary: (data: Uint8Array | ArrayBuffer) => ws.send(data),
close: (code?: number, reason?: string) => ws.close(code, reason),
ping: (data?: Uint8Array) => ws.ping(data),
terminate: () => ws.terminate(),
context: {} as any,
data: new Map(),
};
}
}

3
ts/adapters/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { BaseAdapter } from './adapter.base.js';
export type { IAdapterCharacteristics, TRequestHandler } from './adapter.base.js';
export { AdapterFactory } from './adapter.factory.js';