import Connection from "./Connection"; import {hashCode} from "./utils"; /** * The connection manager keeps track of active connections (WebSocket connections, see Connection). * * Its refresh() method reconciles state changes with the target state by closing/opening connections * as required. This is done pretty much exactly the same way as in the Android app. */ class ConnectionManager { constructor() { this.connections = new Map(); // ConnectionId -> Connection (hash, see below) this.stateListener = null; // Fired when connection state changes this.messageListener = null; // Fired when new notifications arrive } registerStateListener(listener) { this.stateListener = listener; } resetStateListener() { this.stateListener = null; } registerMessageListener(listener) { this.messageListener = listener; } resetMessageListener() { this.messageListener = null; } /** * This function figures out which websocket connections should be running by comparing the * current state of the world (connections) with the target state (targetIds). * * It uses a "connectionId", which is sha256($subscriptionId|$username|$password) to identify * connections. If any of them change, the connection is closed/replaced. */ async refresh(subscriptions, users) { if (!subscriptions || !users) { return; } console.log(`[ConnectionManager] Refreshing connections`); const subscriptionsWithUsersAndConnectionId = await Promise.all(subscriptions .map(async s => { const [user] = users.filter(u => u.baseUrl === s.baseUrl); const connectionId = await makeConnectionId(s, user); return {...s, user, connectionId}; })); const targetIds = subscriptionsWithUsersAndConnectionId.map(s => s.connectionId); const deletedIds = Array.from(this.connections.keys()).filter(id => !targetIds.includes(id)); // Create and add new connections subscriptionsWithUsersAndConnectionId.forEach(subscription => { const subscriptionId = subscription.id; const connectionId = subscription.connectionId; const added = !this.connections.get(connectionId) if (added) { const baseUrl = subscription.baseUrl; const topic = subscription.topic; const user = subscription.user; const since = subscription.last; const connection = new Connection( connectionId, subscriptionId, baseUrl, topic, user, since, (subscriptionId, notification) => this.notificationReceived(subscriptionId, notification), (subscriptionId, state) => this.stateChanged(subscriptionId, state) ); this.connections.set(connectionId, connection); console.log(`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`); connection.start(); } }); // Delete old connections deletedIds.forEach(id => { console.log(`[ConnectionManager] Closing connection ${id}`); const connection = this.connections.get(id); this.connections.delete(id); connection.close(); }); } stateChanged(subscriptionId, state) { if (this.stateListener) { try { this.stateListener(subscriptionId, state); } catch (e) { console.error(`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, e); } } } notificationReceived(subscriptionId, notification) { if (this.messageListener) { try { this.messageListener(subscriptionId, notification); } catch (e) { console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e); } } } } const makeConnectionId = async (subscription, user) => { return (user) ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); } const connectionManager = new ConnectionManager(); export default connectionManager;