Merge pull request 'socketio-fallback-signalR' (#1) from socketio-fallback-signalR into main

Reviewed-on: pierre/SimpleConnect-client-electron#1
This commit is contained in:
2025-09-24 18:09:08 +00:00
5 changed files with 362 additions and 68 deletions

117
connection-manager.js Normal file
View File

@@ -0,0 +1,117 @@
/**
* Gestionnaire de connexion avec fallback SignalR → WebSocket
* Essaie d'abord SignalR, puis bascule sur WebSocket si nécessaire
*/
const signalR = require('@microsoft/signalr');
const WebSocketAdapter = require('./websocket-adapter');
class ConnectionManager {
constructor(serverUrl, options = {}) {
this.serverUrl = serverUrl;
this.options = options;
this.connection = null;
this.isUsingWebSocketFallback = false;
this.connectionType = 'none';
}
/**
* Établit la connexion en essayant SignalR puis WebSocket
*/
async connect() {
console.log('🔌 Tentative de connexion au serveur...');
// 1. Essayer SignalR d'abord
try {
console.log('📡 Essai avec SignalR...');
this.connection = await this.createSignalRConnection();
await this.connection.start();
this.connectionType = 'SignalR';
this.isUsingWebSocketFallback = false;
console.log('✅ Connexion SignalR établie');
return this.connection;
} catch (signalRError) {
console.warn('⚠️ SignalR indisponible:', signalRError.message);
console.log('🔄 Basculement vers WebSocket...');
}
// 2. Fallback vers WebSocket
try {
this.connection = new WebSocketAdapter(this.serverUrl, this.options);
await this.connection.start();
this.connectionType = 'WebSocket';
this.isUsingWebSocketFallback = true;
console.log('✅ Connexion WebSocket établie (mode fallback)');
return this.connection;
} catch (wsError) {
console.error('❌ Impossible de se connecter (SignalR et WebSocket ont échoué)');
throw wsError;
}
}
/**
* Crée une connexion SignalR
*/
createSignalRConnection() {
const url = `http://${this.serverUrl}/signalR`;
console.log(`Creating SignalR connection to: ${url}`);
return new signalR.HubConnectionBuilder()
.withUrl(url)
.withAutomaticReconnect([0, 2000, 5000, 10000])
.configureLogging(signalR.LogLevel.Information)
.build();
}
/**
* Retourne la connexion active
*/
getConnection() {
return this.connection;
}
/**
* Déconnecte proprement
*/
async disconnect() {
if (this.connection) {
await this.connection.stop();
this.connection = null;
this.connectionType = 'none';
}
}
/**
* Informations sur la connexion
*/
getConnectionInfo() {
return {
type: this.connectionType,
isWebSocketFallback: this.isUsingWebSocketFallback,
isConnected: this.connection && this.connection.state === 'Connected',
serverUrl: this.serverUrl
};
}
/**
* Méthode helper pour invoquer des méthodes sur la connexion
*/
async invoke(methodName, ...args) {
if (!this.connection) {
throw new Error('Pas de connexion active');
}
return this.connection.invoke(methodName, ...args);
}
/**
* Méthode helper pour écouter des événements
*/
on(eventName, handler) {
if (!this.connection) {
throw new Error('Pas de connexion active');
}
this.connection.on(eventName, handler);
}
}
module.exports = ConnectionManager;

View File

@@ -1,5 +1,34 @@
# Changelog - SimpleConnect Electron
## [1.3.0] - 2025-09-12
### Ajouté
- **Support dual SignalR/SocketIO avec fallback automatique** : Compatibilité totale avec backends .NET et Python
- ConnectionManager qui essaie d'abord SignalR puis bascule sur SocketIO
- WebSocketAdapter qui émule l'API SignalR complète avec socket.io-client
- Abstraction totale : même API peu importe le protocole utilisé
- Détection automatique du type de serveur disponible
- Messages de statut indiquant le type de connexion active (SignalR ou WebSocket)
### Modifié
- **Architecture de connexion refactorisée** : Système modulaire avec adaptateurs
- Nouveau module `connection-manager.js` pour gérer la stratégie de fallback
- Nouveau module `websocket-adapter.js` pour l'émulation SignalR avec SocketIO
- Code principal simplifié grâce à l'abstraction de connexion
- Meilleure gestion des erreurs et reconnexion automatique
### Technique
- Ajout de la dépendance `socket.io-client` v4.8.1
- Pattern Adapter pour unifier les APIs SignalR et SocketIO
- Gestion des promesses pour les invocations asynchrones
- Mapping automatique des événements entre les deux protocoles
- Conservation de la compatibilité ascendante avec les serveurs existants
### Documentation
- Support confirmé pour les backends Python/FastAPI avec SocketIO
- Migration transparente entre serveurs .NET et Python
- Logs détaillés du type de connexion utilisé
## [1.2.16] - 2025-09-05
### Ajouté

105
main.js
View File

@@ -3,6 +3,7 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
const signalR = require('@microsoft/signalr');
const ConnectionManager = require('./connection-manager');
let mainWindow;
let config;
@@ -87,58 +88,25 @@ function createWindow() {
});
}
// === GESTION SIGNALR ===
// === GESTION SIGNALR/WEBSOCKET ===
function initializeSignalR() {
if (!config.signalR || !config.signalR.enabled) {
console.log('SignalR désactivé dans la configuration');
console.log('SignalR/WebSocket désactivé dans la configuration');
signalRStatus = 'disabled';
sendSignalRStatus();
return;
}
try {
// Créer la connexion SignalR
signalRConnection = new signalR.HubConnectionBuilder()
.withUrl(config.signalR.serverUrl)
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.configureLogging(signalR.LogLevel.Information)
.build();
// Utiliser le ConnectionManager avec fallback automatique SignalR → WebSocket
const connectionManager = new ConnectionManager(config.ServerIp || config.signalR.serverUrl.replace('http://', '').replace('/signalR', ''));
// Gérer les changements d'état
signalRConnection.onreconnecting(() => {
console.log('SignalR: Reconnexion en cours...');
logSignalR('🔄 SignalR en reconnexion...', {
previousStatus: signalRStatus,
timestamp: new Date().toISOString()
});
signalRStatus = 'connecting';
sendSignalRStatus();
});
// La connexion sera établie plus tard avec fallback automatique
signalRConnection = connectionManager;
signalRConnection.onreconnected(() => {
console.log('SignalR: Reconnecté');
logSignalR('🔗 SignalR reconnecté avec succès', {
connectionId: signalRConnection.connectionId,
timestamp: new Date().toISOString()
});
signalRStatus = 'connected';
sendSignalRStatus();
});
// Les handlers d'état seront configurés après la connexion
signalRConnection.onclose(() => {
console.log('SignalR: Connexion fermée');
logSignalR('🔌 SignalR déconnecté', {
lastConnectionId: signalRConnection.connectionId,
timestamp: new Date().toISOString()
});
signalRStatus = 'disconnected';
sendSignalRStatus();
});
// Configurer les méthodes SignalR
setupSignalRMethods();
// Démarrer la connexion
// Démarrer la connexion (les handlers seront configurés après)
startSignalRConnection();
} catch (error) {
@@ -148,7 +116,14 @@ function initializeSignalR() {
}
}
function setupSignalRMethods() {
function setupSignalRHandlers() {
// Configuration des handlers après la connexion
const connection = signalRConnection.getConnection();
if (!connection) {
console.error('Pas de connexion active pour configurer les handlers');
return;
}
// === LOGGER UNIVERSEL POUR TOUS LES MESSAGES SIGNALR ===
// Intercepter TOUS les messages reçus du serveur pour découvrir les événements disponibles
@@ -185,7 +160,7 @@ function setupSignalRMethods() {
// Écouter tous les événements possibles et logger ce qu'on reçoit
possibleEvents.forEach(eventName => {
signalRConnection.on(eventName, (...args) => {
connection.on(eventName, (...args) => {
// Logger dans la console avec formatage
console.log('═══════════════════════════════════════════════════════════');
console.log(`📨 MESSAGE SIGNALR REÇU: ${eventName}`);
@@ -323,18 +298,28 @@ async function startSignalRConnection() {
try {
signalRStatus = 'connecting';
sendSignalRStatus();
logSignalR('🔌 Tentative de connexion SignalR...', {
serverUrl: config.signalR.serverUrl,
logSignalR('🔌 Tentative de connexion au serveur...', {
serverUrl: config.ServerIp || config.signalR.serverUrl,
status: 'connecting'
});
await signalRConnection.start();
console.log('SignalR: Connexion établie');
logSignalR('✅ Connexion SignalR établie avec succès', {
connectionId: signalRConnection.connectionId,
// Le ConnectionManager gère le fallback automatiquement
const connection = await signalRConnection.connect();
// Déterminer quel type de connexion a réussi
const connectionInfo = signalRConnection.getConnectionInfo();
console.log(`Connexion établie via ${connectionInfo.type}`);
logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, {
connectionType: connectionInfo.type,
isWebSocketFallback: connectionInfo.isWebSocketFallback,
status: 'connected',
serverUrl: config.signalR.serverUrl
serverUrl: connectionInfo.serverUrl
});
// Maintenant configurer les handlers sur la connexion active
setupSignalRHandlers();
signalRStatus = 'connected';
sendSignalRStatus();
@@ -403,10 +388,9 @@ app.on('window-all-closed', async () => {
}
}
// Arrêter SignalR
if (signalRConnection) {
await signalRConnection.stop();
}
// Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage
// Le serveur .NET ne supporte pas ce message
// On laisse la connexion se fermer naturellement avec l'application
if (process.platform !== 'darwin') {
app.quit();
@@ -655,15 +639,10 @@ ipcMain.handle('logout', async () => {
// Handler pour quitter l'application proprement
ipcMain.handle('quit-app', async () => {
// Fermer la connexion SignalR si elle existe
if (signalRConnection) {
try {
await signalRConnection.stop();
console.log('Connexion SignalR fermée');
} catch (error) {
console.error('Erreur lors de la fermeture de SignalR:', error);
}
}
// Ne pas appeler disconnect() pour éviter l'envoi du CloseMessage
// Le serveur .NET ne supporte pas ce message
// On laisse la connexion se fermer naturellement avec l'application
// (comme le fait le client de prod)
// Quitter l'application
app.quit();

View File

@@ -1,6 +1,6 @@
{
"name": "simpleconnect-electron",
"version": "1.2.16",
"version": "1.3.0",
"description": "Application de gestion centralisée des plannings médicaux pour centres d'appels",
"main": "main.js",
"scripts": {
@@ -86,6 +86,7 @@
},
"dependencies": {
"@microsoft/signalr": "^9.0.6",
"choices.js": "^11.1.0"
"choices.js": "^11.1.0",
"socket.io-client": "^4.8.1"
}
}

168
websocket-adapter.js Normal file
View File

@@ -0,0 +1,168 @@
/**
* Adaptateur WebSocket pour fallback quand SignalR n'est pas disponible
* Émule l'API SignalR avec SocketIO
*/
const io = require('socket.io-client');
class WebSocketAdapter {
constructor(serverUrl, options = {}) {
// Nettoyer l'URL et s'assurer qu'elle est valide
this.serverUrl = serverUrl;
if (this.serverUrl.includes('/signalR')) {
this.serverUrl = this.serverUrl.replace('/signalR', '');
}
// S'assurer qu'on a le protocole
if (!this.serverUrl.startsWith('http://') && !this.serverUrl.startsWith('https://')) {
this.serverUrl = `http://${this.serverUrl}`;
}
this.socket = null;
this.connected = false;
this.handlers = new Map();
this.options = options;
this.pendingInvocations = new Map();
this.invocationId = 0;
}
/**
* Émule connection.start() de SignalR
*/
async start() {
return new Promise((resolve, reject) => {
console.log(`🔄 Connexion WebSocket à ${this.serverUrl}...`);
// Se connecter avec SocketIO
this.socket = io(this.serverUrl, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 2000,
...this.options
});
this.socket.on('connect', () => {
console.log('✅ WebSocket connecté (mode fallback)');
this.connected = true;
// Émuler l'événement 'connected' de SignalR
if (this.handlers.has('connected')) {
this.handlers.get('connected')({ connectionId: this.socket.id });
}
resolve();
});
this.socket.on('connect_error', (error) => {
console.error('❌ Erreur connexion WebSocket:', error.message);
reject(error);
});
this.socket.on('disconnect', (reason) => {
console.log('🔌 WebSocket déconnecté:', reason);
this.connected = false;
});
// Timeout de connexion
setTimeout(() => {
if (!this.connected) {
reject(new Error('Timeout de connexion WebSocket'));
}
}, 10000);
});
}
/**
* Émule connection.stop() de SignalR
*/
async stop() {
if (this.socket) {
this.socket.disconnect();
this.connected = false;
console.log('🛑 WebSocket fermé');
}
}
/**
* Émule connection.invoke() de SignalR
*/
async invoke(methodName, ...args) {
return new Promise((resolve, reject) => {
if (!this.connected) {
reject(new Error('WebSocket non connecté'));
return;
}
const invocationId = ++this.invocationId;
console.log(`📤 WebSocket invoke: ${methodName}`, args);
// Stocker la promesse pour la résolution
this.pendingInvocations.set(invocationId, { resolve, reject });
// Émuler le format SignalR mais utiliser l'API SocketIO
// SocketIO utilise emit avec un callback pour les réponses
this.socket.emit(methodName, ...args, (response) => {
console.log(`📥 WebSocket response for ${methodName}:`, response);
const pending = this.pendingInvocations.get(invocationId);
if (pending) {
pending.resolve(response);
this.pendingInvocations.delete(invocationId);
}
});
// Timeout pour éviter les promesses pendantes
setTimeout(() => {
const pending = this.pendingInvocations.get(invocationId);
if (pending) {
pending.reject(new Error(`Timeout invoking ${methodName}`));
this.pendingInvocations.delete(invocationId);
}
}, 30000);
});
}
/**
* Émule connection.on() de SignalR
*/
on(eventName, handler) {
console.log(`👂 WebSocket listener ajouté: ${eventName}`);
this.handlers.set(eventName, handler);
// Mapper les événements SocketIO vers les handlers
if (this.socket) {
this.socket.on(eventName, (...args) => {
console.log(`📨 WebSocket event reçu: ${eventName}`, args);
handler(eventName, args);
});
}
}
/**
* Émule connection.off() de SignalR
*/
off(eventName) {
this.handlers.delete(eventName);
if (this.socket) {
this.socket.off(eventName);
}
}
/**
* État de la connexion (émule SignalR HubConnectionState)
*/
get state() {
if (this.connected) {
return 'Connected';
}
return 'Disconnected';
}
/**
* Vérifier si la connexion utilise WebSocket (toujours vrai pour cet adaptateur)
*/
get isWebSocketFallback() {
return true;
}
}
module.exports = WebSocketAdapter;