- Updated test files to use new testing library and reduced test cycles for efficiency. - Removed dependency on smartexpress and integrated direct WebSocket handling. - Enhanced Smartsocket and SmartsocketClient classes to support new message types and authentication flow. - Implemented a new message interface for structured communication between client and server. - Added external server support for smartserve with appropriate WebSocket hooks. - Improved connection management and error handling in SocketConnection and SocketRequest classes. - Cleaned up code and removed deprecated socket.io references in favor of native WebSocket.
312 lines
9.5 KiB
TypeScript
312 lines
9.5 KiB
TypeScript
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: WebSocket | pluginsTyped.ws.WebSocket;
|
|
}
|
|
|
|
/**
|
|
* interface for authentication data
|
|
*/
|
|
export interface ISocketConnectionAuthenticationObject {
|
|
alias: string;
|
|
}
|
|
|
|
// export classes
|
|
export let allSocketConnections = new plugins.lik.ObjectMap<SocketConnection>();
|
|
|
|
/**
|
|
* 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: WebSocket | pluginsTyped.ws.WebSocket;
|
|
|
|
public eventSubject = new plugins.smartrx.rxjs.Subject<interfaces.TConnectionStatus>();
|
|
public eventStatus: interfaces.TConnectionStatus = 'new';
|
|
|
|
private tagStore: interfaces.TTagStore = {};
|
|
public tagStoreObservable = new plugins.smartrx.rxjs.Subject<interfaces.TTagStore>();
|
|
public remoteTagStoreObservable = new plugins.smartrx.rxjs.Subject<interfaces.TTagStore>();
|
|
|
|
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);
|
|
break;
|
|
case 'functionResponse':
|
|
this.handleFunctionResponse(messageData);
|
|
break;
|
|
case 'tagUpdate':
|
|
this.handleTagUpdate(messageData);
|
|
break;
|
|
default:
|
|
// Authentication messages are handled by the server/client classes
|
|
break;
|
|
}
|
|
}
|
|
|
|
private handleFunctionCall(messageData: interfaces.ISocketMessage): void {
|
|
const requestData: ISocketRequestDataObject<any> = {
|
|
funcCallData: {
|
|
funcName: messageData.payload.funcName,
|
|
funcDataArg: messageData.payload.funcData,
|
|
},
|
|
shortId: messageData.id,
|
|
};
|
|
|
|
const referencedFunction: SocketFunction<any> =
|
|
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<any> = {
|
|
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
|
|
* @param tagIdArg
|
|
*/
|
|
public async getTagById(tagIdArg: interfaces.ITag['id']) {
|
|
return this.tagStore[tagIdArg];
|
|
}
|
|
|
|
/**
|
|
* removes a tag from a connection
|
|
*/
|
|
public async removeTagById(tagIdArg: interfaces.ITag['id']) {
|
|
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<SocketConnection> {
|
|
const done = plugins.smartpromise.defer<SocketConnection>();
|
|
|
|
// Set up message handler for authentication
|
|
const messageHandler = (event: MessageEvent | { data: string }) => {
|
|
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) {
|
|
// Not a valid message, ignore
|
|
}
|
|
};
|
|
|
|
this.socket.addEventListener('message', messageHandler as any);
|
|
|
|
// 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: MessageEvent | { data: string }) => {
|
|
try {
|
|
const data = typeof event.data === 'string' ? event.data : event.data.toString();
|
|
const message: interfaces.ISocketMessage = JSON.parse(data);
|
|
this.handleMessage(message);
|
|
} catch (err) {
|
|
// Not a valid JSON message, ignore
|
|
}
|
|
};
|
|
|
|
this.socket.addEventListener('message', messageHandler as any);
|
|
|
|
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;
|
|
}
|
|
}
|