feat: add REST + Socket.IO fallback for Python server
Add RestSocketAdapter that uses: - REST API for actions (login, logout, terminals) - Socket.IO for real-time events (IpbxEvent) ConnectionManager now tries SignalR first (.NET server), then falls back to REST+SocketIO (Python server). This enables the client to work with both servers during migration.
This commit is contained in:
@@ -1,60 +1,66 @@
|
|||||||
/**
|
/**
|
||||||
* Gestionnaire de connexion avec fallback SignalR → WebSocket
|
* Gestionnaire de connexion avec fallback SignalR → REST + Socket.IO
|
||||||
* Essaie d'abord SignalR, puis bascule sur WebSocket si nécessaire
|
*
|
||||||
|
* 1. Essaie d'abord SignalR (serveur .NET legacy)
|
||||||
|
* 2. Si SignalR échoue, bascule sur REST + Socket.IO (serveur Python)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const signalR = require('@microsoft/signalr');
|
const signalR = require('@microsoft/signalr');
|
||||||
const WebSocketAdapter = require('./websocket-adapter');
|
const RestSocketAdapter = require('./rest-socket-adapter');
|
||||||
|
|
||||||
class ConnectionManager {
|
class ConnectionManager {
|
||||||
constructor(serverUrl, options = {}) {
|
constructor(serverUrl, options = {}) {
|
||||||
this.serverUrl = serverUrl;
|
this.serverUrl = serverUrl;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.connection = null;
|
this.connection = null;
|
||||||
this.isUsingWebSocketFallback = false;
|
this.connectionType = 'none'; // 'none', 'SignalR', 'REST+SocketIO'
|
||||||
this.connectionType = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Établit la connexion en essayant SignalR puis WebSocket
|
* Établit la connexion en essayant SignalR puis REST + Socket.IO
|
||||||
*/
|
*/
|
||||||
async connect() {
|
async connect() {
|
||||||
console.log('🔌 Tentative de connexion au serveur...');
|
console.log('🔌 Tentative de connexion au serveur...');
|
||||||
|
|
||||||
// 1. Essayer SignalR d'abord
|
// 1. Essayer SignalR d'abord (serveur .NET)
|
||||||
try {
|
try {
|
||||||
console.log('📡 Essai avec SignalR...');
|
console.log('📡 Essai avec SignalR...');
|
||||||
this.connection = await this.createSignalRConnection();
|
this.connection = this._createSignalRConnection();
|
||||||
await this.connection.start();
|
await this.connection.start();
|
||||||
this.connectionType = 'SignalR';
|
this.connectionType = 'SignalR';
|
||||||
this.isUsingWebSocketFallback = false;
|
console.log('✅ Connexion SignalR établie (serveur .NET)');
|
||||||
console.log('✅ Connexion SignalR établie');
|
|
||||||
return this.connection;
|
return this.connection;
|
||||||
} catch (signalRError) {
|
} catch (signalRError) {
|
||||||
console.warn('⚠️ SignalR indisponible:', signalRError.message);
|
console.warn('⚠️ SignalR indisponible:', signalRError.message);
|
||||||
console.log('🔄 Basculement vers WebSocket...');
|
console.log('🔄 Basculement vers REST + Socket.IO...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback vers WebSocket
|
// 2. Fallback vers REST + Socket.IO (serveur Python)
|
||||||
try {
|
try {
|
||||||
this.connection = new WebSocketAdapter(this.serverUrl, this.options);
|
this.connection = new RestSocketAdapter(this.serverUrl, this.options);
|
||||||
await this.connection.start();
|
await this.connection.start();
|
||||||
this.connectionType = 'WebSocket';
|
this.connectionType = 'REST+SocketIO';
|
||||||
this.isUsingWebSocketFallback = true;
|
console.log('✅ Connexion REST + Socket.IO établie (serveur Python)');
|
||||||
console.log('✅ Connexion WebSocket établie (mode fallback)');
|
|
||||||
return this.connection;
|
return this.connection;
|
||||||
} catch (wsError) {
|
} catch (restError) {
|
||||||
console.error('❌ Impossible de se connecter (SignalR et WebSocket ont échoué)');
|
console.error('❌ REST + Socket.IO indisponible:', restError.message);
|
||||||
throw wsError;
|
throw new Error('Impossible de se connecter (SignalR et REST+SocketIO ont échoué)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée une connexion SignalR
|
* Crée une connexion SignalR
|
||||||
*/
|
*/
|
||||||
createSignalRConnection() {
|
_createSignalRConnection() {
|
||||||
const url = `http://${this.serverUrl}/signalR`;
|
let url = this.serverUrl;
|
||||||
console.log(`Creating SignalR connection to: ${url}`);
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
url = `http://${url}`;
|
||||||
|
}
|
||||||
|
if (!url.includes('/signalR')) {
|
||||||
|
url = `${url}/signalR`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📡 URL SignalR: ${url}`);
|
||||||
|
|
||||||
return new signalR.HubConnectionBuilder()
|
return new signalR.HubConnectionBuilder()
|
||||||
.withUrl(url)
|
.withUrl(url)
|
||||||
@@ -87,7 +93,8 @@ class ConnectionManager {
|
|||||||
getConnectionInfo() {
|
getConnectionInfo() {
|
||||||
return {
|
return {
|
||||||
type: this.connectionType,
|
type: this.connectionType,
|
||||||
isWebSocketFallback: this.isUsingWebSocketFallback,
|
isSignalR: this.connectionType === 'SignalR',
|
||||||
|
isRestSocketIO: this.connectionType === 'REST+SocketIO',
|
||||||
isConnected: this.connection && this.connection.state === 'Connected',
|
isConnected: this.connection && this.connection.state === 'Connected',
|
||||||
serverUrl: this.serverUrl
|
serverUrl: this.serverUrl
|
||||||
};
|
};
|
||||||
@@ -112,6 +119,15 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
this.connection.on(eventName, handler);
|
this.connection.on(eventName, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode helper pour retirer un handler d'événement
|
||||||
|
*/
|
||||||
|
off(eventName) {
|
||||||
|
if (this.connection && this.connection.off) {
|
||||||
|
this.connection.off(eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ConnectionManager;
|
module.exports = ConnectionManager;
|
||||||
3
main.js
3
main.js
@@ -317,7 +317,8 @@ async function startSignalRConnection() {
|
|||||||
console.log(`Connexion établie via ${connectionInfo.type}`);
|
console.log(`Connexion établie via ${connectionInfo.type}`);
|
||||||
logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, {
|
logSignalR(`✅ Connexion établie avec succès (${connectionInfo.type})`, {
|
||||||
connectionType: connectionInfo.type,
|
connectionType: connectionInfo.type,
|
||||||
isWebSocketFallback: connectionInfo.isWebSocketFallback,
|
isSignalR: connectionInfo.isSignalR,
|
||||||
|
isRestSocketIO: connectionInfo.isRestSocketIO,
|
||||||
status: 'connected',
|
status: 'connected',
|
||||||
serverUrl: connectionInfo.serverUrl
|
serverUrl: connectionInfo.serverUrl
|
||||||
});
|
});
|
||||||
|
|||||||
285
rest-socket-adapter.js
Normal file
285
rest-socket-adapter.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* Adaptateur REST + Socket.IO pour le serveur Python
|
||||||
|
*
|
||||||
|
* Utilise REST pour les actions (login, logout, terminaux)
|
||||||
|
* et Socket.IO pour les événements temps réel (IpbxEvent)
|
||||||
|
*
|
||||||
|
* Émule l'API SignalR pour compatibilité avec le code existant
|
||||||
|
*/
|
||||||
|
|
||||||
|
const io = require('socket.io-client');
|
||||||
|
|
||||||
|
class RestSocketAdapter {
|
||||||
|
constructor(serverUrl, options = {}) {
|
||||||
|
// Nettoyer l'URL
|
||||||
|
this.serverUrl = serverUrl;
|
||||||
|
if (this.serverUrl.includes('/signalR')) {
|
||||||
|
this.serverUrl = this.serverUrl.replace('/signalR', '');
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Token JWT pour les requêtes authentifiées
|
||||||
|
this.authToken = null;
|
||||||
|
this.currentAgent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Émule connection.start() de SignalR
|
||||||
|
* Établit la connexion Socket.IO pour les événements
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log(`[RestSocketAdapter] Connexion à ${this.serverUrl}...`);
|
||||||
|
|
||||||
|
// Vérifier d'abord que le serveur Python répond
|
||||||
|
fetch(`${this.serverUrl}/health`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Health check failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(health => {
|
||||||
|
console.log(`[RestSocketAdapter] Serveur Python détecté:`, health);
|
||||||
|
|
||||||
|
// Connecter Socket.IO pour les événements temps réel
|
||||||
|
this.socket = io(this.serverUrl, {
|
||||||
|
path: '/socket.io/',
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
|
reconnectionDelay: 2000,
|
||||||
|
...this.options
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('connect', () => {
|
||||||
|
console.log('[RestSocketAdapter] Socket.IO connecté');
|
||||||
|
this.connected = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('connect_error', (error) => {
|
||||||
|
console.error('[RestSocketAdapter] Erreur Socket.IO:', error.message);
|
||||||
|
// On reste connecté même si Socket.IO échoue (REST fonctionne)
|
||||||
|
if (!this.connected) {
|
||||||
|
this.connected = true;
|
||||||
|
console.log('[RestSocketAdapter] Mode REST seul (Socket.IO indisponible)');
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('disconnect', (reason) => {
|
||||||
|
console.log('[RestSocketAdapter] Socket.IO déconnecté:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configurer les handlers d'événements existants
|
||||||
|
this.handlers.forEach((handler, eventName) => {
|
||||||
|
this.socket.on(eventName, (...args) => {
|
||||||
|
console.log(`[RestSocketAdapter] Événement reçu: ${eventName}`, args);
|
||||||
|
handler(eventName, args);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout de connexion
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.connected) {
|
||||||
|
reject(new Error('Timeout de connexion au serveur Python'));
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[RestSocketAdapter] Serveur Python non disponible:', error.message);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Émule connection.stop() de SignalR
|
||||||
|
*/
|
||||||
|
async stop() {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.disconnect();
|
||||||
|
}
|
||||||
|
this.connected = false;
|
||||||
|
this.authToken = null;
|
||||||
|
this.currentAgent = null;
|
||||||
|
console.log('[RestSocketAdapter] Déconnecté');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Émule connection.invoke() de SignalR
|
||||||
|
* Redirige vers les endpoints REST appropriés
|
||||||
|
*/
|
||||||
|
async invoke(methodName, ...args) {
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new Error('Non connecté au serveur');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[RestSocketAdapter] invoke: ${methodName}`, args);
|
||||||
|
|
||||||
|
switch (methodName) {
|
||||||
|
case 'GetTerminalListByServiceProvider':
|
||||||
|
return this._getTerminalList(args[0]);
|
||||||
|
|
||||||
|
case 'AgentLogin':
|
||||||
|
return this._agentLogin(args[0], args[1], args[2]);
|
||||||
|
|
||||||
|
case 'AgentLogoff':
|
||||||
|
return this._agentLogoff(args[0]);
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`[RestSocketAdapter] Méthode non implémentée: ${methodName}`);
|
||||||
|
throw new Error(`Méthode non supportée: ${methodName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/terminals/{serviceProvider}
|
||||||
|
*/
|
||||||
|
async _getTerminalList(serviceProvider) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.serverUrl}/api/v1/terminals/${encodeURIComponent(serviceProvider)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||||
|
throw new Error(error.detail || 'Erreur récupération terminaux');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
// Retourner juste la liste des numéros de terminaux (format SignalR)
|
||||||
|
return data.terminals.map(t => t.number || t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/auth/login
|
||||||
|
*/
|
||||||
|
async _agentLogin(email, password, terminal) {
|
||||||
|
const response = await fetch(`${this.serverUrl}/api/v1/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
access_code: email,
|
||||||
|
password: password,
|
||||||
|
terminal: terminal,
|
||||||
|
force_disconnect: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||||
|
throw new Error(error.detail || 'Erreur de connexion');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Stocker le token si présent
|
||||||
|
if (data.token) {
|
||||||
|
this.authToken = data.token;
|
||||||
|
}
|
||||||
|
this.currentAgent = data;
|
||||||
|
|
||||||
|
// Enregistrer le terminal sur Socket.IO pour recevoir les événements
|
||||||
|
if (this.socket && this.socket.connected) {
|
||||||
|
this.socket.emit('register', {
|
||||||
|
terminal: terminal,
|
||||||
|
agentId: data.accessCode || data.access_code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retourner au format attendu par le client (compatible SignalR)
|
||||||
|
return {
|
||||||
|
accessCode: data.accessCode || data.access_code,
|
||||||
|
firstName: data.firstName || data.first_name,
|
||||||
|
lastName: data.lastName || data.last_name,
|
||||||
|
connList: data.connList || data.conn_list || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/auth/logout
|
||||||
|
*/
|
||||||
|
async _agentLogoff(accessCode) {
|
||||||
|
// Désenregistrer du Socket.IO
|
||||||
|
if (this.socket && this.socket.connected) {
|
||||||
|
this.socket.emit('unregister', { terminal: this.currentAgent?.terminal });
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter le token si disponible
|
||||||
|
if (this.authToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${this.serverUrl}/api/v1/auth/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify({ access_code: accessCode })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||||
|
throw new Error(error.detail || 'Erreur de déconnexion');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authToken = null;
|
||||||
|
this.currentAgent = null;
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Émule connection.on() de SignalR
|
||||||
|
*/
|
||||||
|
on(eventName, handler) {
|
||||||
|
console.log(`[RestSocketAdapter] Enregistrement handler: ${eventName}`);
|
||||||
|
this.handlers.set(eventName, handler);
|
||||||
|
|
||||||
|
// Si Socket.IO est déjà connecté, ajouter le handler
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.on(eventName, (...args) => {
|
||||||
|
console.log(`[RestSocketAdapter] Événement 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() {
|
||||||
|
return this.connected ? 'Connected' : 'Disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifie ce type d'adaptateur
|
||||||
|
*/
|
||||||
|
get isRestSocketAdapter() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RestSocketAdapter;
|
||||||
Reference in New Issue
Block a user