162 lines
5.1 KiB
TypeScript
162 lines
5.1 KiB
TypeScript
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(),
|
|
};
|
|
}
|
|
}
|