Files
smartserve/ts/adapters/adapter.bun.ts

162 lines
5.1 KiB
TypeScript
Raw Normal View History

2025-11-29 15:24:00 +00:00
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(),
};
}
}