Refactor smartsocket implementation for improved WebSocket handling and message protocol
- 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.
This commit is contained in:
@@ -26,7 +26,7 @@ export interface ISocketConnectionConstructorOptions {
|
||||
authenticated: boolean;
|
||||
side: TSocketConnectionSide;
|
||||
smartsocketHost: Smartsocket | SmartsocketClient;
|
||||
socket: pluginsTyped.socketIo.Socket | pluginsTyped.socketIoClient.Socket;
|
||||
socket: WebSocket | pluginsTyped.ws.WebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ export class SocketConnection {
|
||||
public side: TSocketConnectionSide;
|
||||
public authenticated: boolean = false;
|
||||
public smartsocketRef: Smartsocket | SmartsocketClient;
|
||||
public socket: pluginsTyped.socketIo.Socket | pluginsTyped.socketIoClient.Socket;
|
||||
public socket: WebSocket | pluginsTyped.ws.WebSocket;
|
||||
|
||||
public eventSubject = new plugins.smartrx.rxjs.Subject<interfaces.TConnectionStatus>();
|
||||
public eventStatus: interfaces.TConnectionStatus = 'new';
|
||||
@@ -65,20 +65,94 @@ export class SocketConnection {
|
||||
|
||||
// standard behaviour that is always true
|
||||
allSocketConnections.add(this);
|
||||
}
|
||||
|
||||
// handle connection
|
||||
this.socket.on('connect', async () => {
|
||||
this.updateStatus('connected');
|
||||
});
|
||||
this.socket.on('disconnect', async () => {
|
||||
logger.log(
|
||||
'info',
|
||||
`SocketConnection with >alias ${this.alias} on >side ${this.side} disconnected`
|
||||
);
|
||||
await this.disconnect();
|
||||
allSocketConnections.remove(this);
|
||||
this.eventSubject.next('disconnected');
|
||||
});
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +173,10 @@ export class SocketConnection {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
this.socket.emit('updateTagStore', this.tagStore);
|
||||
this.sendMessage({
|
||||
type: 'tagUpdate',
|
||||
payload: { tags: this.tagStore },
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
|
||||
@@ -117,36 +194,68 @@ export class SocketConnection {
|
||||
public async removeTagById(tagIdArg: interfaces.ITag['id']) {
|
||||
delete this.tagStore[tagIdArg];
|
||||
this.tagStoreObservable.next(this.tagStore);
|
||||
this.socket.emit('updateTagStore', this.tagStore);
|
||||
this.sendMessage({
|
||||
type: 'tagUpdate',
|
||||
payload: { tags: this.tagStore },
|
||||
});
|
||||
}
|
||||
|
||||
// authenticating --------------------------
|
||||
|
||||
/**
|
||||
* authenticate the socket
|
||||
* authenticate the socket (server side)
|
||||
*/
|
||||
public authenticate() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
this.socket.on('dataAuth', async (dataArg: ISocketConnectionAuthenticationObject) => {
|
||||
logger.log('info', 'received authentication data...');
|
||||
this.socket.removeAllListeners('dataAuth');
|
||||
if (dataArg.alias) {
|
||||
// TODO: authenticate password
|
||||
this.alias = dataArg.alias;
|
||||
this.authenticated = true;
|
||||
this.socket.emit('authenticated');
|
||||
logger.log('ok', `socket with >>alias ${this.alias} is authenticated!`);
|
||||
done.resolve(this);
|
||||
} else {
|
||||
this.authenticated = false;
|
||||
await this.disconnect();
|
||||
done.reject('a socket tried to connect, but could not authenticated.');
|
||||
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
|
||||
}
|
||||
});
|
||||
const requestAuthPayload: interfaces.IRequestAuthPayload = {
|
||||
serverAlias: this.smartsocketRef.alias,
|
||||
};
|
||||
this.socket.emit('requestAuth', requestAuthPayload);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -158,43 +267,18 @@ export class SocketConnection {
|
||||
public listenToFunctionRequests() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
if (this.authenticated) {
|
||||
this.socket.on('function', (dataArg: ISocketRequestDataObject<any>) => {
|
||||
// check if requested function is available to the socket's scope
|
||||
// logger.log('info', 'function request received');
|
||||
const referencedFunction: SocketFunction<any> =
|
||||
this.smartsocketRef.socketFunctions.findSync((socketFunctionArg) => {
|
||||
return socketFunctionArg.name === dataArg.funcCallData.funcName;
|
||||
});
|
||||
if (referencedFunction) {
|
||||
// logger.log('ok', 'function in access scope');
|
||||
const localSocketRequest = new SocketRequest(this.smartsocketRef, {
|
||||
side: 'responding',
|
||||
originSocketConnection: this,
|
||||
shortId: dataArg.shortId,
|
||||
funcCallData: dataArg.funcCallData,
|
||||
});
|
||||
localSocketRequest.createResponse(); // takes care of creating response and sending it back
|
||||
} else {
|
||||
logger.log('warn', 'function not existent or out of access scope');
|
||||
// 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.on('functionResponse', (dataArg: ISocketRequestDataObject<any>) => {
|
||||
// logger.log('info', `received response for request with id ${dataArg.shortId}`);
|
||||
const targetSocketRequest = SocketRequest.getSocketRequestById(
|
||||
this.smartsocketRef,
|
||||
dataArg.shortId
|
||||
);
|
||||
targetSocketRequest.handleResponse(dataArg);
|
||||
});
|
||||
};
|
||||
|
||||
this.socket.on('updateTagStore', async (tagStoreArg: interfaces.TTagStore) => {
|
||||
if (!plugins.smartjson.deepEqualObjects(this.tagStore, tagStoreArg)) {
|
||||
this.tagStore = tagStoreArg;
|
||||
this.socket.emit('updateTagStore', this.tagStore);
|
||||
this.tagStoreObservable.next(this.tagStore);
|
||||
}
|
||||
this.remoteTagStoreObservable.next(tagStoreArg);
|
||||
});
|
||||
this.socket.addEventListener('message', messageHandler as any);
|
||||
|
||||
logger.log(
|
||||
'info',
|
||||
@@ -211,7 +295,10 @@ export class SocketConnection {
|
||||
|
||||
// disconnecting ----------------------
|
||||
public async disconnect() {
|
||||
this.socket.disconnect(true);
|
||||
if (this.socket.readyState === 1 || this.socket.readyState === 0) {
|
||||
this.socket.close();
|
||||
}
|
||||
allSocketConnections.remove(this);
|
||||
this.updateStatus('disconnected');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user