import * as plugins from './smartsocket.plugins.js'; import * as pluginsTyped from './smartsocket.pluginstyped.js'; import * as interfaces from './interfaces/index.js'; // import classes import { Smartsocket } from './smartsocket.classes.smartsocket.js'; import { SocketFunction } from './smartsocket.classes.socketfunction.js'; import { SocketRequest, type ISocketRequestDataObject } from './smartsocket.classes.socketrequest.js'; // socket.io import { SmartsocketClient } from './smartsocket.classes.smartsocketclient.js'; import { logger } from './smartsocket.logging.js'; // export interfaces /** * defines is a SocketConnection is server or client side. Important for mesh setups. */ export type TSocketConnectionSide = 'server' | 'client'; /** * interface for constructor of class SocketConnection */ export interface ISocketConnectionConstructorOptions { alias: string; authenticated: boolean; side: TSocketConnectionSide; smartsocketHost: Smartsocket | SmartsocketClient; socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike; } /** * interface for authentication data */ export interface ISocketConnectionAuthenticationObject { alias: string; } // export classes export let allSocketConnections = new plugins.lik.ObjectMap(); /** * class SocketConnection represents a websocket connection */ export class SocketConnection { public alias: string; public side: TSocketConnectionSide; public authenticated: boolean = false; public smartsocketRef: Smartsocket | SmartsocketClient; public socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike; public eventSubject = new plugins.smartrx.rxjs.Subject(); public eventStatus: interfaces.TConnectionStatus = 'new'; private tagStore: interfaces.TTagStore = {}; public tagStoreObservable = new plugins.smartrx.rxjs.Subject(); public remoteTagStoreObservable = new plugins.smartrx.rxjs.Subject(); constructor(optionsArg: ISocketConnectionConstructorOptions) { this.alias = optionsArg.alias; this.authenticated = optionsArg.authenticated; this.side = optionsArg.side; this.smartsocketRef = optionsArg.smartsocketHost; this.socket = optionsArg.socket; // standard behaviour that is always true allSocketConnections.add(this); } /** * Sends a message through the socket */ public sendMessage(message: interfaces.ISocketMessage): void { if (this.socket.readyState === 1) { // WebSocket.OPEN this.socket.send(JSON.stringify(message)); } } /** * Handles incoming messages */ public handleMessage(messageData: interfaces.ISocketMessage): void { switch (messageData.type) { case 'function': this.handleFunctionCall(messageData as interfaces.ISocketMessage); break; case 'functionResponse': this.handleFunctionResponse(messageData as interfaces.ISocketMessage); break; case 'tagUpdate': this.handleTagUpdate(messageData as interfaces.ISocketMessage); break; default: // Authentication messages are handled by the server/client classes break; } } private handleFunctionCall(messageData: interfaces.ISocketMessage): void { const requestData: ISocketRequestDataObject = { funcCallData: { funcName: messageData.payload.funcName, funcDataArg: messageData.payload.funcData, }, shortId: messageData.id, }; const referencedFunction: SocketFunction = this.smartsocketRef.socketFunctions.findSync((socketFunctionArg) => { return socketFunctionArg.name === requestData.funcCallData.funcName; }); if (referencedFunction) { const localSocketRequest = new SocketRequest(this.smartsocketRef, { side: 'responding', originSocketConnection: this, shortId: requestData.shortId, funcCallData: requestData.funcCallData, }); localSocketRequest.createResponse(); } else { logger.log('warn', `function ${requestData.funcCallData.funcName} not found or out of scope`); } } private handleFunctionResponse(messageData: interfaces.ISocketMessage): void { const responseData: ISocketRequestDataObject = { funcCallData: { funcName: messageData.payload.funcName, funcDataArg: messageData.payload.funcData, }, shortId: messageData.id, }; const targetSocketRequest = SocketRequest.getSocketRequestById( this.smartsocketRef, responseData.shortId ); if (targetSocketRequest) { targetSocketRequest.handleResponse(responseData); } } private handleTagUpdate(messageData: interfaces.ISocketMessage): void { const tagStoreArg = messageData.payload.tags as interfaces.TTagStore; if (!plugins.smartjson.deepEqualObjects(this.tagStore, tagStoreArg)) { this.tagStore = tagStoreArg; // Echo back to confirm this.sendMessage({ type: 'tagUpdate', payload: { tags: this.tagStore }, }); this.tagStoreObservable.next(this.tagStore); } this.remoteTagStoreObservable.next(tagStoreArg); } /** * adds a tag to a connection */ public async addTag(tagArg: interfaces.ITag) { const done = plugins.smartpromise.defer(); this.tagStore[tagArg.id] = tagArg; this.tagStoreObservable.next(this.tagStore); const remoteSubscription = this.remoteTagStoreObservable.subscribe((remoteTagStore) => { if (!remoteTagStore[tagArg.id]) { return; } const localTagString = plugins.smartjson.stringify(tagArg); const remoteTagString = plugins.smartjson.stringify(remoteTagStore[tagArg.id]); if (localTagString === remoteTagString) { remoteSubscription.unsubscribe(); done.resolve(); } }); this.sendMessage({ type: 'tagUpdate', payload: { tags: this.tagStore }, }); await done.promise; } /** * Gets a tag by id */ public getTagById(tagIdArg: interfaces.ITag['id']): interfaces.ITag | undefined { return this.tagStore[tagIdArg]; } /** * Removes a tag from a connection */ public removeTagById(tagIdArg: interfaces.ITag['id']): void { delete this.tagStore[tagIdArg]; this.tagStoreObservable.next(this.tagStore); this.sendMessage({ type: 'tagUpdate', payload: { tags: this.tagStore }, }); } // authenticating -------------------------- /** * authenticate the socket (server side) */ public authenticate(): Promise { const done = plugins.smartpromise.defer(); // Set up message handler for authentication const messageHandler = (event: pluginsTyped.TMessageEvent) => { try { const data = typeof event.data === 'string' ? event.data : event.data.toString(); const message: interfaces.ISocketMessage = JSON.parse(data); if (message.type === 'auth') { const authData = message.payload as interfaces.IAuthPayload; logger.log('info', 'received authentication data...'); if (authData.alias) { this.alias = authData.alias; this.authenticated = true; // Send authentication response this.sendMessage({ type: 'authResponse', payload: { success: true }, }); logger.log('ok', `socket with >>alias ${this.alias} is authenticated!`); done.resolve(this); } else { this.authenticated = false; this.sendMessage({ type: 'authResponse', payload: { success: false, error: 'No alias provided' }, }); this.disconnect(); done.reject('a socket tried to connect, but could not authenticate.'); } } } catch (err) { logger.log('warn', `Failed to parse auth message: ${err instanceof Error ? err.message : String(err)}`); } }; (this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler); // Request authentication const requestAuthPayload: interfaces.TAuthRequestMessage = { type: 'authRequest', payload: { serverAlias: (this.smartsocketRef as Smartsocket).alias, }, }; this.sendMessage(requestAuthPayload); return done.promise; } // listening ------------------------------- /** * listen to function requests */ public listenToFunctionRequests() { const done = plugins.smartpromise.defer(); if (this.authenticated) { // Set up message handler for all messages const messageHandler = (event: pluginsTyped.TMessageEvent) => { try { const data = typeof event.data === 'string' ? event.data : event.data.toString(); const message: interfaces.ISocketMessage = JSON.parse(data); this.handleMessage(message); } catch (err) { logger.log('warn', `Failed to parse socket message: ${err instanceof Error ? err.message : String(err)}`); } }; (this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler); logger.log( 'info', `now listening to function requests for ${this.alias} on side ${this.side}` ); done.resolve(this); } else { const errMessage = 'socket needs to be authenticated first'; logger.log('error', errMessage); done.reject(errMessage); } return done.promise; } // disconnecting ---------------------- public async disconnect() { if (this.socket.readyState === 1 || this.socket.readyState === 0) { this.socket.close(); } allSocketConnections.remove(this); this.updateStatus('disconnected'); } private updateStatus(statusArg: interfaces.TConnectionStatus) { if (this.eventStatus !== statusArg) { this.eventSubject.next(statusArg); } this.eventStatus = statusArg; } }