2025-12-02 12:13:46 +00:00
|
|
|
import type { ISmartServeInstance, IConnectionInfo, IWebSocketPeer, IWebSocketConnectionCallbacks } from '../core/smartserve.interfaces.js';
|
2025-11-29 15:24:00 +00:00
|
|
|
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);
|
|
|
|
|
|
2025-12-02 12:13:46 +00:00
|
|
|
// Get internal callbacks if typedRouter mode
|
|
|
|
|
const callbacks = (hooks as any)._connectionCallbacks as IWebSocketConnectionCallbacks | undefined;
|
|
|
|
|
const typedRouter = hooks.typedRouter;
|
|
|
|
|
|
2025-11-29 15:24:00 +00:00
|
|
|
socket.onopen = () => {
|
2025-12-02 12:13:46 +00:00
|
|
|
// Register connection if typedRouter mode
|
|
|
|
|
if (callbacks) {
|
|
|
|
|
callbacks.onRegister(peer);
|
|
|
|
|
}
|
2025-11-29 15:24:00 +00:00
|
|
|
hooks.onOpen?.(peer);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-02 12:13:46 +00:00
|
|
|
socket.onmessage = async (event) => {
|
|
|
|
|
// If typedRouter is configured, route through it
|
|
|
|
|
if (typedRouter && typeof event.data === 'string') {
|
|
|
|
|
try {
|
|
|
|
|
const requestObj = JSON.parse(event.data);
|
2025-12-03 23:47:46 +00:00
|
|
|
// Attach peer to localData so TypedHandlers can access the connection
|
|
|
|
|
requestObj.localData = { ...requestObj.localData, peer };
|
2025-12-02 12:13:46 +00:00
|
|
|
const response = await typedRouter.routeAndAddResponse(requestObj);
|
|
|
|
|
if (response) {
|
|
|
|
|
peer.send(JSON.stringify(response));
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('TypedRouter message handling error:', error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Legacy mode: use onMessage hook
|
|
|
|
|
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);
|
|
|
|
|
}
|
2025-11-29 15:24:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onclose = (event) => {
|
2025-12-02 12:13:46 +00:00
|
|
|
// Unregister connection if typedRouter mode
|
|
|
|
|
if (callbacks) {
|
|
|
|
|
callbacks.onUnregister(peer.id);
|
|
|
|
|
}
|
2025-11-29 15:24:00 +00:00
|
|
|
hooks.onClose?.(peer, event.code, event.reason);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onerror = (event) => {
|
|
|
|
|
hooks.onError?.(peer, new Error('WebSocket error'));
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:13:46 +00:00
|
|
|
private createWebSocketPeer(socket: WebSocket, request: Request): IWebSocketPeer {
|
2025-11-29 15:24:00 +00:00
|
|
|
const url = this.parseUrl(request);
|
|
|
|
|
return {
|
|
|
|
|
id: crypto.randomUUID(),
|
|
|
|
|
url: url.pathname,
|
2025-12-02 12:13:46 +00:00
|
|
|
get readyState() { return socket.readyState as 0 | 1 | 2 | 3; },
|
2025-11-29 15:24:00 +00:00
|
|
|
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(),
|
2025-12-02 12:13:46 +00:00
|
|
|
context: {} as any,
|
2025-11-29 15:24:00 +00:00
|
|
|
data: new Map(),
|
2025-12-02 12:13:46 +00:00
|
|
|
tags: new Set<string>(),
|
2025-11-29 15:24:00 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|