feat(websocket): Add TypedRouter WebSocket integration, connection registry, peer tagging and broadcast APIs
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { ISmartServeInstance, IConnectionInfo } from '../core/smartserve.interfaces.js';
|
||||
import type { ISmartServeInstance, IConnectionInfo, IWebSocketPeer, IWebSocketConnectionCallbacks } from '../core/smartserve.interfaces.js';
|
||||
import { BaseAdapter, type IAdapterCharacteristics, type TRequestHandler } from './adapter.base.js';
|
||||
|
||||
// Bun types (for type checking without requiring Bun runtime)
|
||||
@@ -46,9 +46,17 @@ export class BunAdapter extends BaseAdapter {
|
||||
this.stats.requestsActive++;
|
||||
|
||||
try {
|
||||
// Handle WebSocket upgrade
|
||||
// Handle WebSocket upgrade - store data for persistence across events
|
||||
if (this.options.websocket && request.headers.get('upgrade') === 'websocket') {
|
||||
const upgraded = server.upgrade(request);
|
||||
const peerId = crypto.randomUUID();
|
||||
const upgraded = server.upgrade(request, {
|
||||
data: {
|
||||
id: peerId,
|
||||
url: new URL(request.url).pathname,
|
||||
customData: new Map(),
|
||||
tags: new Set<string>(),
|
||||
},
|
||||
});
|
||||
if (upgraded) {
|
||||
return undefined; // Bun handles the upgrade
|
||||
}
|
||||
@@ -108,33 +116,65 @@ export class BunAdapter extends BaseAdapter {
|
||||
const hooks = this.options.websocket;
|
||||
if (!hooks) return undefined;
|
||||
|
||||
// Get internal callbacks if typedRouter mode
|
||||
const callbacks = (hooks as any)._connectionCallbacks as IWebSocketConnectionCallbacks | undefined;
|
||||
const typedRouter = hooks.typedRouter;
|
||||
|
||||
return {
|
||||
open: (ws: any) => {
|
||||
const peer = this.wrapBunWebSocket(ws);
|
||||
// Register connection if typedRouter mode
|
||||
if (callbacks) {
|
||||
callbacks.onRegister(peer);
|
||||
}
|
||||
hooks.onOpen?.(peer);
|
||||
},
|
||||
message: (ws: any, message: string | ArrayBuffer) => {
|
||||
|
||||
message: async (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);
|
||||
|
||||
// If typedRouter is configured, route through it
|
||||
if (typedRouter && typeof message === 'string') {
|
||||
try {
|
||||
const requestObj = JSON.parse(message);
|
||||
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 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);
|
||||
// Unregister connection if typedRouter mode
|
||||
if (callbacks) {
|
||||
callbacks.onUnregister(peer.id);
|
||||
}
|
||||
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));
|
||||
@@ -142,10 +182,18 @@ export class BunAdapter extends BaseAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
private wrapBunWebSocket(ws: any): any {
|
||||
private wrapBunWebSocket(ws: any): IWebSocketPeer {
|
||||
// IMPORTANT: Use persistent data from ws.data since Bun re-wraps on each event
|
||||
const wsData = ws.data || {
|
||||
id: crypto.randomUUID(),
|
||||
url: '',
|
||||
customData: new Map(),
|
||||
tags: new Set<string>(),
|
||||
};
|
||||
|
||||
return {
|
||||
id: ws.data?.id ?? crypto.randomUUID(),
|
||||
url: ws.data?.url ?? '',
|
||||
id: wsData.id,
|
||||
url: wsData.url,
|
||||
get readyState() { return ws.readyState; },
|
||||
protocol: ws.protocol ?? '',
|
||||
extensions: ws.extensions ?? '',
|
||||
@@ -155,7 +203,8 @@ export class BunAdapter extends BaseAdapter {
|
||||
ping: (data?: Uint8Array) => ws.ping(data),
|
||||
terminate: () => ws.terminate(),
|
||||
context: {} as any,
|
||||
data: ws.data?.customData ?? new Map(),
|
||||
data: wsData.customData,
|
||||
tags: wsData.tags, // Reference to persistent Set
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user