initial
This commit is contained in:
132
ts/adapters/adapter.base.ts
Normal file
132
ts/adapters/adapter.base.ts
Normal 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
161
ts/adapters/adapter.bun.ts
Normal 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
141
ts/adapters/adapter.deno.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
77
ts/adapters/adapter.factory.ts
Normal file
77
ts/adapters/adapter.factory.ts
Normal 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
317
ts/adapters/adapter.node.ts
Normal 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
3
ts/adapters/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user