feat(typedsocket): Add SmartServe integration, tagging and improved client reconnect/backoff; update deps and tests
This commit is contained in:
@@ -1,13 +1,37 @@
|
||||
import * as plugins from './typedsocket.plugins.js';
|
||||
|
||||
const publicRoleName = 'publicRoleName';
|
||||
const publicRolePass = 'publicRolePass';
|
||||
|
||||
export type TTypedSocketSide = 'server' | 'client';
|
||||
export type TConnectionStatus = 'new' | 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
|
||||
|
||||
const TAG_PREFIX = '__typedsocket_tag__';
|
||||
|
||||
/**
|
||||
* Internal TypedRequest interfaces for tag management
|
||||
*/
|
||||
interface IReq_SetClientTag extends plugins.typedrequestInterfaces.ITypedRequest {
|
||||
method: '__typedsocket_setTag';
|
||||
request: { name: string; payload: any };
|
||||
response: { success: boolean };
|
||||
}
|
||||
|
||||
interface IReq_RemoveClientTag extends plugins.typedrequestInterfaces.ITypedRequest {
|
||||
method: '__typedsocket_removeTag';
|
||||
request: { name: string };
|
||||
response: { success: boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a TypedSocket client
|
||||
*/
|
||||
export interface ITypedSocketClientOptions {
|
||||
autoReconnect?: boolean;
|
||||
maxRetries?: number;
|
||||
initialBackoffMs?: number;
|
||||
maxBackoffMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for SmartServe's IWebSocketPeer to provide tag compatibility
|
||||
* SmartServe uses Set<string> for tags, while TypedSocket uses {id, payload} format
|
||||
*/
|
||||
export interface ISmartServeConnectionWrapper {
|
||||
peer: plugins.IWebSocketPeer;
|
||||
@@ -18,7 +42,6 @@ export interface ISmartServeConnectionWrapper {
|
||||
* Creates a wrapper around IWebSocketPeer for tag compatibility
|
||||
*/
|
||||
function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper {
|
||||
const TAG_PREFIX = '__typedsocket_tag__';
|
||||
return {
|
||||
peer,
|
||||
async getTagById(tagId: string): Promise<{ id: string; payload: any } | undefined> {
|
||||
@@ -32,109 +55,62 @@ function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnection
|
||||
}
|
||||
|
||||
export class TypedSocket {
|
||||
// STATIC
|
||||
// ============================================================================
|
||||
// STATIC METHODS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* creates a typedsocket server
|
||||
* note: this will fail in browser environments as server libs are not bundled.
|
||||
* Creates a TypedSocket client using native WebSocket.
|
||||
* Works in both browser and Node.js environments.
|
||||
*
|
||||
* @param typedrouterArg - TypedRouter for handling server-initiated requests
|
||||
* @param serverUrlArg - Server URL (e.g., 'http://localhost:3000' or 'wss://example.com')
|
||||
* @param options - Connection options
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const typedRouter = new TypedRouter();
|
||||
* const client = await TypedSocket.createClient(
|
||||
* typedRouter,
|
||||
* 'http://localhost:3000',
|
||||
* { autoReconnect: true }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
public static async createServer(
|
||||
typedrouterArg: plugins.typedrequest.TypedRouter
|
||||
): Promise<TypedSocket> {
|
||||
const smartsocketServer = new plugins.smartsocket.Smartsocket({
|
||||
alias: 'typedsocketServer',
|
||||
port: 3000,
|
||||
});
|
||||
|
||||
smartsocketServer.socketFunctions.add(
|
||||
new plugins.smartsocket.SocketFunction({
|
||||
funcName: 'processMessage',
|
||||
funcDef: async (dataArg, socketConnectionArg) => {
|
||||
return typedrouterArg.routeAndAddResponse(dataArg);
|
||||
},
|
||||
})
|
||||
);
|
||||
const typedsocket = new TypedSocket(
|
||||
'server',
|
||||
typedrouterArg,
|
||||
async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
dataArg: T,
|
||||
targetConnectionArg?: plugins.smartsocket.SocketConnection
|
||||
): Promise<T> => {
|
||||
if (!targetConnectionArg) {
|
||||
if ((smartsocketServer.socketConnections.getArray().length = 1)) {
|
||||
console.log(
|
||||
'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
|
||||
);
|
||||
targetConnectionArg = smartsocketServer.socketConnections.getArray()[0];
|
||||
} else {
|
||||
throw new Error(
|
||||
'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
|
||||
);
|
||||
}
|
||||
}
|
||||
const response: T = (await smartsocketServer.clientCall(
|
||||
'processMessage',
|
||||
dataArg,
|
||||
targetConnectionArg
|
||||
)) as any;
|
||||
return response;
|
||||
},
|
||||
smartsocketServer
|
||||
);
|
||||
await smartsocketServer.start();
|
||||
|
||||
return typedsocket;
|
||||
}
|
||||
|
||||
public static async createClient(
|
||||
typedrouterArg: plugins.typedrequest.TypedRouter,
|
||||
serverUrlArg: string,
|
||||
aliasArg = 'clientArg'
|
||||
options: ITypedSocketClientOptions = {}
|
||||
): Promise<TypedSocket> {
|
||||
const domain = new plugins.smartstring.Domain(serverUrlArg);
|
||||
|
||||
const socketOptions: plugins.smartsocket.ISmartsocketClientOptions = {
|
||||
alias: aliasArg,
|
||||
port: domain.port || 3000,
|
||||
url: `${domain.nodeParsedUrl.protocol}//${domain.nodeParsedUrl.hostname}`,
|
||||
const defaultOptions: Required<ITypedSocketClientOptions> = {
|
||||
autoReconnect: true,
|
||||
maxRetries: 100,
|
||||
initialBackoffMs: 1000,
|
||||
maxBackoffMs: 60000,
|
||||
};
|
||||
console.log(`starting typedsocket with the following settings:`);
|
||||
console.log(socketOptions);
|
||||
const smartsocketClient = new plugins.smartsocket.SmartsocketClient(socketOptions);
|
||||
smartsocketClient.addSocketFunction(
|
||||
new plugins.smartsocket.SocketFunction({
|
||||
funcName: 'processMessage',
|
||||
funcDef: async (dataArg, socketConnectionArg) => {
|
||||
return typedrouterArg.routeAndAddResponse(dataArg);
|
||||
},
|
||||
})
|
||||
);
|
||||
const typedsocket = new TypedSocket(
|
||||
'client',
|
||||
typedrouterArg,
|
||||
async <T extends plugins.typedrequestInterfaces.ITypedRequest>(dataArg: T): Promise<T> => {
|
||||
const response: T = smartsocketClient.serverCall('processMessage', dataArg) as any as T;
|
||||
return response;
|
||||
},
|
||||
smartsocketClient
|
||||
);
|
||||
console.log(`typedsocket triggering smartsocket to connect...`);
|
||||
const before = Date.now();
|
||||
await smartsocketClient.connect();
|
||||
console.log(`typedsocket triggered smartsocket connected in ${Date.now() - before}ms!!!`)
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
|
||||
return typedsocket;
|
||||
}
|
||||
const typedSocket = new TypedSocket('client', typedrouterArg);
|
||||
typedSocket.clientOptions = opts;
|
||||
typedSocket.serverUrl = serverUrlArg;
|
||||
typedSocket.currentBackoff = opts.initialBackoffMs;
|
||||
|
||||
public static useWindowLocationOriginUrl = () => {
|
||||
const windowLocationResult = plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString();
|
||||
return windowLocationResult;
|
||||
await typedSocket.connect();
|
||||
|
||||
return typedSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current window location origin URL.
|
||||
* Useful in browser environments for connecting to the same origin.
|
||||
*/
|
||||
public static useWindowLocationOriginUrl = (): string => {
|
||||
return plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a TypedSocket server from an existing SmartServe instance.
|
||||
* Use this when you want TypedSocket to work with SmartServe's WebSocket handling.
|
||||
* This is the only way to create a server-side TypedSocket.
|
||||
*
|
||||
* @param smartServeArg - SmartServe instance with typedRouter configured in websocket options
|
||||
* @param typedRouterArg - TypedRouter for handling requests (must match SmartServe's typedRouter)
|
||||
@@ -159,249 +135,479 @@ export class TypedSocket {
|
||||
): TypedSocket {
|
||||
const connectionWrappers = new Map<string, ISmartServeConnectionWrapper>();
|
||||
|
||||
// Create the postMethod for server-initiated requests
|
||||
const postMethod = async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
dataArg: T,
|
||||
targetConnectionArg?: ISmartServeConnectionWrapper
|
||||
): Promise<T> => {
|
||||
if (!targetConnectionArg) {
|
||||
const allConnections = smartServeArg.getWebSocketConnections();
|
||||
if (allConnections.length === 1) {
|
||||
console.log(
|
||||
'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
|
||||
);
|
||||
const peer = allConnections[0];
|
||||
let wrapper = connectionWrappers.get(peer.id);
|
||||
if (!wrapper) {
|
||||
wrapper = wrapSmartServePeer(peer);
|
||||
connectionWrappers.set(peer.id, wrapper);
|
||||
}
|
||||
targetConnectionArg = wrapper;
|
||||
} else if (allConnections.length === 0) {
|
||||
throw new Error('No WebSocket connections available');
|
||||
} else {
|
||||
throw new Error(
|
||||
'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Register interest for the response using correlation ID
|
||||
const interest = await typedRouterArg.fireEventInterestMap.addInterest(
|
||||
dataArg.correlation.id,
|
||||
dataArg
|
||||
);
|
||||
|
||||
// Send the request to the client
|
||||
targetConnectionArg.peer.send(plugins.smartjson.stringify(dataArg));
|
||||
|
||||
// Wait for the response (TypedRouter will fulfill via routeAndAddResponse when response arrives)
|
||||
const response = await interest.interestFullfilled as T;
|
||||
return response;
|
||||
};
|
||||
|
||||
const typedSocket = new TypedSocket(
|
||||
'server',
|
||||
typedRouterArg,
|
||||
postMethod as any,
|
||||
null as any // No smartsocket server/client when using SmartServe
|
||||
);
|
||||
// Register built-in tag handlers
|
||||
TypedSocket.registerTagHandlers(typedRouterArg);
|
||||
|
||||
const typedSocket = new TypedSocket('server', typedRouterArg);
|
||||
typedSocket.smartServeRef = smartServeArg;
|
||||
typedSocket.smartServeConnectionWrappers = connectionWrappers;
|
||||
|
||||
return typedSocket;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
public side: TTypedSocketSide;
|
||||
public typedrouter: plugins.typedrequest.TypedRouter;
|
||||
/**
|
||||
* Registers built-in TypedHandlers for tag management
|
||||
*/
|
||||
private static registerTagHandlers(typedRouter: plugins.typedrequest.TypedRouter): void {
|
||||
// Set tag handler
|
||||
typedRouter.addTypedHandler<IReq_SetClientTag>(
|
||||
new plugins.typedrequest.TypedHandler('__typedsocket_setTag', async (data, meta) => {
|
||||
const peer = meta?.localData?.peer as plugins.IWebSocketPeer;
|
||||
if (!peer) {
|
||||
console.warn('setTag: No peer found in request context');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// SmartServe mode properties
|
||||
private smartServeRef?: plugins.SmartServe;
|
||||
private smartServeConnectionWrappers: Map<string, ISmartServeConnectionWrapper> = new Map();
|
||||
peer.tags.add(data.name);
|
||||
peer.data.set(`${TAG_PREFIX}${data.name}`, data.payload);
|
||||
|
||||
public get eventSubject(): plugins.smartrx.rxjs.Subject<plugins.smartsocket.TConnectionStatus> {
|
||||
if (this.smartServeRef) {
|
||||
// SmartServe doesn't provide an eventSubject, return a new Subject
|
||||
// In SmartServe mode, connection events are handled via onConnectionOpen/onConnectionClose hooks
|
||||
console.warn('eventSubject is not fully supported in SmartServe mode. Use SmartServe hooks instead.');
|
||||
return new plugins.smartrx.rxjs.Subject();
|
||||
}
|
||||
return this.socketServerOrClient.eventSubject;
|
||||
return { success: true };
|
||||
})
|
||||
);
|
||||
|
||||
// Remove tag handler
|
||||
typedRouter.addTypedHandler<IReq_RemoveClientTag>(
|
||||
new plugins.typedrequest.TypedHandler('__typedsocket_removeTag', async (data, meta) => {
|
||||
const peer = meta?.localData?.peer as plugins.IWebSocketPeer;
|
||||
if (!peer) {
|
||||
console.warn('removeTag: No peer found in request context');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
peer.tags.delete(data.name);
|
||||
peer.data.delete(`${TAG_PREFIX}${data.name}`);
|
||||
|
||||
return { success: true };
|
||||
})
|
||||
);
|
||||
}
|
||||
private postMethod: plugins.typedrequest.IPostMethod &
|
||||
((
|
||||
typedRequestPostObject: plugins.typedrequestInterfaces.ITypedRequest,
|
||||
socketConnectionArg?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
|
||||
) => Promise<plugins.typedrequestInterfaces.ITypedRequest>);
|
||||
private socketServerOrClient:
|
||||
| plugins.smartsocket.Smartsocket
|
||||
| plugins.smartsocket.SmartsocketClient;
|
||||
constructor(
|
||||
|
||||
// ============================================================================
|
||||
// INSTANCE PROPERTIES
|
||||
// ============================================================================
|
||||
|
||||
public readonly side: TTypedSocketSide;
|
||||
public readonly typedrouter: plugins.typedrequest.TypedRouter;
|
||||
|
||||
// Connection status observable
|
||||
public statusSubject = new plugins.smartrx.rxjs.Subject<TConnectionStatus>();
|
||||
private connectionStatus: TConnectionStatus = 'new';
|
||||
|
||||
// Client-specific properties
|
||||
private websocket: WebSocket | null = null;
|
||||
private clientOptions: Required<ITypedSocketClientOptions> | null = null;
|
||||
private serverUrl: string = '';
|
||||
private retryCount = 0;
|
||||
private currentBackoff = 1000;
|
||||
private pendingRequests = new Map<string, {
|
||||
resolve: (response: any) => void;
|
||||
reject: (error: Error) => void;
|
||||
}>();
|
||||
|
||||
// Server-specific properties (SmartServe mode)
|
||||
private smartServeRef: plugins.SmartServe | null = null;
|
||||
private smartServeConnectionWrappers = new Map<string, ISmartServeConnectionWrapper>();
|
||||
|
||||
// ============================================================================
|
||||
// CONSTRUCTOR
|
||||
// ============================================================================
|
||||
|
||||
private constructor(
|
||||
sideArg: TTypedSocketSide,
|
||||
typedrouterArg: plugins.typedrequest.TypedRouter,
|
||||
postMethodArg: plugins.typedrequest.IPostMethod,
|
||||
socketServerOrClientArg: plugins.smartsocket.Smartsocket | plugins.smartsocket.SmartsocketClient
|
||||
typedrouterArg: plugins.typedrequest.TypedRouter
|
||||
) {
|
||||
this.side = sideArg;
|
||||
this.typedrouter = typedrouterArg;
|
||||
this.postMethod = postMethodArg;
|
||||
this.socketServerOrClient = socketServerOrClientArg;
|
||||
}
|
||||
|
||||
public addTag<T extends plugins.typedrequestInterfaces.ITag = any>(
|
||||
nameArg: T['name'],
|
||||
payloadArg: T['payload']
|
||||
) {
|
||||
if (
|
||||
this.side === 'client' &&
|
||||
this.socketServerOrClient instanceof plugins.smartsocket.SmartsocketClient
|
||||
) {
|
||||
this.socketServerOrClient.socketConnection.addTag({
|
||||
id: nameArg,
|
||||
payload: payloadArg,
|
||||
});
|
||||
} else {
|
||||
throw new Error('tagging is only supported on clients');
|
||||
// ============================================================================
|
||||
// CLIENT METHODS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Connects the client to the server using native WebSocket
|
||||
*/
|
||||
private async connect(): Promise<void> {
|
||||
const done = plugins.smartpromise.defer<void>();
|
||||
|
||||
this.updateStatus('connecting');
|
||||
|
||||
// Convert HTTP URL to WebSocket URL
|
||||
const wsUrl = this.toWebSocketUrl(this.serverUrl);
|
||||
console.log(`TypedSocket connecting to ${wsUrl}...`);
|
||||
|
||||
this.websocket = new WebSocket(wsUrl);
|
||||
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
if (this.connectionStatus !== 'connected') {
|
||||
console.warn('TypedSocket connection timeout');
|
||||
this.websocket?.close();
|
||||
done.reject(new Error('Connection timeout'));
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
console.log('TypedSocket connected!');
|
||||
this.updateStatus('connected');
|
||||
this.retryCount = 0;
|
||||
this.currentBackoff = this.clientOptions?.initialBackoffMs ?? 1000;
|
||||
done.resolve();
|
||||
};
|
||||
|
||||
this.websocket.onmessage = async (event) => {
|
||||
await this.handleMessage(event.data);
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
this.handleDisconnect();
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
console.error('TypedSocket WebSocket error:', error);
|
||||
};
|
||||
|
||||
try {
|
||||
await done.promise;
|
||||
} catch (err) {
|
||||
clearTimeout(connectionTimeout);
|
||||
if (this.clientOptions?.autoReconnect) {
|
||||
await this.scheduleReconnect();
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP(S) URL to a WebSocket URL
|
||||
*/
|
||||
private toWebSocketUrl(url: string): string {
|
||||
const parsed = new URL(url);
|
||||
const wsProtocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${wsProtocol}//${parsed.host}${parsed.pathname}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming WebSocket messages
|
||||
*/
|
||||
private async handleMessage(data: string | ArrayBuffer): Promise<void> {
|
||||
try {
|
||||
const messageText = typeof data === 'string' ? data : new TextDecoder().decode(data);
|
||||
const message = plugins.smartjson.parse(messageText) as plugins.typedrequestInterfaces.ITypedRequest;
|
||||
|
||||
// Check if this is a response to a pending request
|
||||
if (message.correlation?.id && this.pendingRequests.has(message.correlation.id)) {
|
||||
const pending = this.pendingRequests.get(message.correlation.id)!;
|
||||
this.pendingRequests.delete(message.correlation.id);
|
||||
pending.resolve(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-initiated request - route through TypedRouter
|
||||
const response = await this.typedrouter.routeAndAddResponse(message);
|
||||
if (response && this.websocket?.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(plugins.smartjson.stringify(response));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('TypedSocket failed to process message:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles WebSocket disconnection
|
||||
*/
|
||||
private handleDisconnect(): void {
|
||||
if (this.connectionStatus === 'disconnected') {
|
||||
return; // Already handled
|
||||
}
|
||||
|
||||
this.updateStatus('disconnected');
|
||||
|
||||
if (this.clientOptions?.autoReconnect && this.retryCount < this.clientOptions.maxRetries) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a reconnection attempt with exponential backoff
|
||||
*/
|
||||
private async scheduleReconnect(): Promise<void> {
|
||||
if (!this.clientOptions) return;
|
||||
|
||||
this.updateStatus('reconnecting');
|
||||
this.retryCount++;
|
||||
|
||||
// Exponential backoff with jitter
|
||||
const jitter = this.currentBackoff * 0.2 * (Math.random() * 2 - 1);
|
||||
const delay = Math.min(this.currentBackoff + jitter, this.clientOptions.maxBackoffMs);
|
||||
|
||||
console.log(`TypedSocket reconnecting in ${Math.round(delay)}ms (attempt ${this.retryCount}/${this.clientOptions.maxRetries})`);
|
||||
|
||||
await plugins.smartdelay.delayFor(delay);
|
||||
|
||||
// Increase backoff for next time
|
||||
this.currentBackoff = Math.min(this.currentBackoff * 2, this.clientOptions.maxBackoffMs);
|
||||
|
||||
try {
|
||||
await this.connect();
|
||||
} catch (err) {
|
||||
console.error('TypedSocket reconnection failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates connection status and notifies subscribers
|
||||
*/
|
||||
private updateStatus(status: TConnectionStatus): void {
|
||||
if (this.connectionStatus !== status) {
|
||||
this.connectionStatus = status;
|
||||
this.statusSubject.next(status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the server and waits for response (client-side)
|
||||
*/
|
||||
private async sendRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
request: T
|
||||
): Promise<T> {
|
||||
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket not connected');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(request.correlation.id);
|
||||
reject(new Error('Request timeout'));
|
||||
}, 30000);
|
||||
|
||||
this.pendingRequests.set(request.correlation.id, {
|
||||
resolve: (response) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
|
||||
this.websocket!.send(plugins.smartjson.stringify(request));
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PUBLIC API - SHARED
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Creates a TypedRequest for the specified method.
|
||||
* On clients, sends to the server.
|
||||
* On servers, sends to the specified target connection.
|
||||
*/
|
||||
public createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
methodName: T['method'],
|
||||
targetConnection?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
|
||||
targetConnection?: ISmartServeConnectionWrapper
|
||||
): plugins.typedrequest.TypedRequest<T> {
|
||||
const typedrequest = new plugins.typedrequest.TypedRequest<T>(
|
||||
new plugins.typedrequest.TypedTarget({
|
||||
postMethod: async (requestDataArg) => {
|
||||
const result = await this.postMethod(requestDataArg, targetConnection as any);
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
const postMethod = async (requestDataArg: T): Promise<T> => {
|
||||
if (this.side === 'client') {
|
||||
return this.sendRequest(requestDataArg);
|
||||
}
|
||||
|
||||
// Server-side: send to target connection
|
||||
if (!this.smartServeRef) {
|
||||
throw new Error('Server not initialized');
|
||||
}
|
||||
|
||||
let target = targetConnection;
|
||||
if (!target) {
|
||||
const allConnections = this.smartServeRef.getWebSocketConnections();
|
||||
if (allConnections.length === 1) {
|
||||
const peer = allConnections[0];
|
||||
target = this.getOrCreateWrapper(peer);
|
||||
} else if (allConnections.length === 0) {
|
||||
throw new Error('No WebSocket connections available');
|
||||
} else {
|
||||
throw new Error('Multiple connections available - specify targetConnection');
|
||||
}
|
||||
}
|
||||
|
||||
// Register interest for response
|
||||
const interest = await this.typedrouter.fireEventInterestMap.addInterest(
|
||||
requestDataArg.correlation.id,
|
||||
requestDataArg
|
||||
);
|
||||
|
||||
// Send request
|
||||
target.peer.send(plugins.smartjson.stringify(requestDataArg));
|
||||
|
||||
// Wait for response
|
||||
return await interest.interestFullfilled as T;
|
||||
};
|
||||
|
||||
return new plugins.typedrequest.TypedRequest<T>(
|
||||
new plugins.typedrequest.TypedTarget({ postMethod }),
|
||||
methodName
|
||||
);
|
||||
return typedrequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all matching target connections
|
||||
* Works with both Smartsocket and SmartServe backends
|
||||
* @param asyncFindFuncArg - async filter function
|
||||
* @returns array of matching connections
|
||||
* Gets the current connection status
|
||||
*/
|
||||
public getStatus(): TConnectionStatus {
|
||||
return this.connectionStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the TypedSocket client or cleans up server state
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (this.side === 'client') {
|
||||
if (this.clientOptions) {
|
||||
this.clientOptions.autoReconnect = false;
|
||||
}
|
||||
if (this.websocket) {
|
||||
this.websocket.close();
|
||||
this.websocket = null;
|
||||
}
|
||||
this.pendingRequests.clear();
|
||||
} else {
|
||||
// Server mode - just clear wrappers (SmartServe manages its own lifecycle)
|
||||
this.smartServeConnectionWrappers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLIENT-ONLY METHODS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sets a tag on this client connection.
|
||||
* Tags are stored on the server and can be used for filtering.
|
||||
* @client-only
|
||||
*/
|
||||
public async setTag<T extends plugins.typedrequestInterfaces.ITag>(
|
||||
name: T['name'],
|
||||
payload: T['payload']
|
||||
): Promise<void> {
|
||||
if (this.side !== 'client') {
|
||||
throw new Error('setTag is only available on clients');
|
||||
}
|
||||
|
||||
const request = this.createTypedRequest<IReq_SetClientTag>('__typedsocket_setTag');
|
||||
const response = await request.fire({ name, payload });
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to set tag on server');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from this client connection.
|
||||
* @client-only
|
||||
*/
|
||||
public async removeTag(name: string): Promise<void> {
|
||||
if (this.side !== 'client') {
|
||||
throw new Error('removeTag is only available on clients');
|
||||
}
|
||||
|
||||
const request = this.createTypedRequest<IReq_RemoveClientTag>('__typedsocket_removeTag');
|
||||
const response = await request.fire({ name });
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to remove tag on server');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVER-ONLY METHODS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Gets or creates a connection wrapper for a peer
|
||||
*/
|
||||
private getOrCreateWrapper(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper {
|
||||
let wrapper = this.smartServeConnectionWrappers.get(peer.id);
|
||||
if (!wrapper) {
|
||||
wrapper = wrapSmartServePeer(peer);
|
||||
this.smartServeConnectionWrappers.set(peer.id, wrapper);
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all connections matching the filter function.
|
||||
* @server-only
|
||||
*/
|
||||
public async findAllTargetConnections(
|
||||
asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
|
||||
): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
|
||||
// SmartServe mode
|
||||
if (this.smartServeRef) {
|
||||
const matchingConnections: ISmartServeConnectionWrapper[] = [];
|
||||
for (const peer of this.smartServeRef.getWebSocketConnections()) {
|
||||
let wrapper = this.smartServeConnectionWrappers.get(peer.id);
|
||||
if (!wrapper) {
|
||||
wrapper = wrapSmartServePeer(peer);
|
||||
this.smartServeConnectionWrappers.set(peer.id, wrapper);
|
||||
}
|
||||
if (await asyncFindFuncArg(wrapper)) {
|
||||
matchingConnections.push(wrapper);
|
||||
}
|
||||
}
|
||||
return matchingConnections;
|
||||
asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
|
||||
): Promise<ISmartServeConnectionWrapper[]> {
|
||||
if (this.side !== 'server' || !this.smartServeRef) {
|
||||
throw new Error('findAllTargetConnections is only available on servers');
|
||||
}
|
||||
|
||||
// Smartsocket mode
|
||||
if (this.socketServerOrClient instanceof plugins.smartsocket.Smartsocket) {
|
||||
const matchingSockets: plugins.smartsocket.SocketConnection[] = [];
|
||||
for (const socketConnection of this.socketServerOrClient.socketConnections.getArray()) {
|
||||
if (await asyncFindFuncArg(socketConnection)) {
|
||||
matchingSockets.push(socketConnection);
|
||||
}
|
||||
const matchingConnections: ISmartServeConnectionWrapper[] = [];
|
||||
for (const peer of this.smartServeRef.getWebSocketConnections()) {
|
||||
const wrapper = this.getOrCreateWrapper(peer);
|
||||
if (await asyncFindFuncArg(wrapper)) {
|
||||
matchingConnections.push(wrapper);
|
||||
}
|
||||
return matchingSockets;
|
||||
}
|
||||
|
||||
throw new Error('this method >>findTargetConnection<< is only available from the server');
|
||||
return matchingConnections;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a single target connection by returning the first one of all matching ones
|
||||
* @param asyncFindFuncArg
|
||||
* @returns
|
||||
* Finds the first connection matching the filter function.
|
||||
* @server-only
|
||||
*/
|
||||
public async findTargetConnection(
|
||||
asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
|
||||
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
|
||||
asyncFindFuncArg: (connectionArg: ISmartServeConnectionWrapper) => Promise<boolean>
|
||||
): Promise<ISmartServeConnectionWrapper | undefined> {
|
||||
const allMatching = await this.findAllTargetConnections(asyncFindFuncArg);
|
||||
return allMatching[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all connections that have a specific tag
|
||||
* Works with both Smartsocket and SmartServe backends
|
||||
* Finds all connections with the specified tag.
|
||||
* @server-only
|
||||
*/
|
||||
public async findAllTargetConnectionsByTag<
|
||||
TTag extends plugins.typedrequestInterfaces.ITag = any
|
||||
>(keyArg: TTag['name'], payloadArg?: TTag['payload']): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
|
||||
// SmartServe mode - use native filtering for better performance
|
||||
if (this.smartServeRef) {
|
||||
const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg);
|
||||
const results: ISmartServeConnectionWrapper[] = [];
|
||||
|
||||
for (const peer of peers) {
|
||||
let wrapper = this.smartServeConnectionWrappers.get(peer.id);
|
||||
if (!wrapper) {
|
||||
wrapper = wrapSmartServePeer(peer);
|
||||
this.smartServeConnectionWrappers.set(peer.id, wrapper);
|
||||
}
|
||||
|
||||
// If payload specified, also filter by payload stored in peer.data
|
||||
if (payloadArg !== undefined) {
|
||||
const tag = await wrapper.getTagById(keyArg);
|
||||
if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
results.push(wrapper);
|
||||
}
|
||||
return results;
|
||||
public async findAllTargetConnectionsByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
|
||||
keyArg: TTag['name'],
|
||||
payloadArg?: TTag['payload']
|
||||
): Promise<ISmartServeConnectionWrapper[]> {
|
||||
if (this.side !== 'server' || !this.smartServeRef) {
|
||||
throw new Error('findAllTargetConnectionsByTag is only available on servers');
|
||||
}
|
||||
|
||||
// Smartsocket mode - use existing logic
|
||||
return this.findAllTargetConnections(async (socketConnectionArg) => {
|
||||
let result: boolean;
|
||||
if (!payloadArg) {
|
||||
result = !!(await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg));
|
||||
} else {
|
||||
result = !!(
|
||||
plugins.smartjson.stringify((await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg))?.payload) ===
|
||||
plugins.smartjson.stringify(payloadArg)
|
||||
);
|
||||
const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg);
|
||||
const results: ISmartServeConnectionWrapper[] = [];
|
||||
|
||||
for (const peer of peers) {
|
||||
const wrapper = this.getOrCreateWrapper(peer);
|
||||
|
||||
// If payload specified, also filter by payload
|
||||
if (payloadArg !== undefined) {
|
||||
const tag = await wrapper.getTagById(keyArg);
|
||||
if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
results.push(wrapper);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single connection by tag
|
||||
* Finds the first connection with the specified tag.
|
||||
* @server-only
|
||||
*/
|
||||
public async findTargetConnectionByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
|
||||
keyArg: TTag['name'],
|
||||
payloadArg?: TTag['payload']
|
||||
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
|
||||
): Promise<ISmartServeConnectionWrapper | undefined> {
|
||||
const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg);
|
||||
return allResults[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the TypedSocket server/client
|
||||
* Note: In SmartServe mode, SmartServe manages its own lifecycle
|
||||
*/
|
||||
public async stop() {
|
||||
if (this.smartServeRef) {
|
||||
// SmartServe manages its own lifecycle
|
||||
// Clear our connection wrappers
|
||||
this.smartServeConnectionWrappers.clear();
|
||||
return;
|
||||
}
|
||||
await this.socketServerOrClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user