ConnectionManager.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import Connection from "./Connection";
  2. import {hashCode} from "./utils";
  3. /**
  4. * The connection manager keeps track of active connections (WebSocket connections, see Connection).
  5. *
  6. * Its refresh() method reconciles state changes with the target state by closing/opening connections
  7. * as required. This is done pretty much exactly the same way as in the Android app.
  8. */
  9. class ConnectionManager {
  10. constructor() {
  11. this.connections = new Map(); // ConnectionId -> Connection (hash, see below)
  12. this.stateListener = null; // Fired when connection state changes
  13. this.notificationListener = null; // Fired when new notifications arrive
  14. }
  15. registerStateListener(listener) {
  16. this.stateListener = listener;
  17. }
  18. resetStateListener() {
  19. this.stateListener = null;
  20. }
  21. registerNotificationListener(listener) {
  22. this.notificationListener = listener;
  23. }
  24. resetNotificationListener() {
  25. this.notificationListener = null;
  26. }
  27. /**
  28. * This function figures out which websocket connections should be running by comparing the
  29. * current state of the world (connections) with the target state (targetIds).
  30. *
  31. * It uses a "connectionId", which is sha256($subscriptionId|$username|$password) to identify
  32. * connections. If any of them change, the connection is closed/replaced.
  33. */
  34. async refresh(subscriptions, users) {
  35. if (!subscriptions || !users) {
  36. return;
  37. }
  38. console.log(`[ConnectionManager] Refreshing connections`);
  39. const subscriptionsWithUsersAndConnectionId = await Promise.all(subscriptions
  40. .map(async s => {
  41. const [user] = users.filter(u => u.baseUrl === s.baseUrl);
  42. const connectionId = await makeConnectionId(s, user);
  43. return {...s, user, connectionId};
  44. }));
  45. const targetIds = subscriptionsWithUsersAndConnectionId.map(s => s.connectionId);
  46. const deletedIds = Array.from(this.connections.keys()).filter(id => !targetIds.includes(id));
  47. // Create and add new connections
  48. subscriptionsWithUsersAndConnectionId.forEach(subscription => {
  49. const subscriptionId = subscription.id;
  50. const connectionId = subscription.connectionId;
  51. const added = !this.connections.get(connectionId)
  52. if (added) {
  53. const baseUrl = subscription.baseUrl;
  54. const topic = subscription.topic;
  55. const user = subscription.user;
  56. const since = subscription.last;
  57. const connection = new Connection(
  58. connectionId,
  59. subscriptionId,
  60. baseUrl,
  61. topic,
  62. user,
  63. since,
  64. (subscriptionId, notification) => this.notificationReceived(subscriptionId, notification),
  65. (subscriptionId, state) => this.stateChanged(subscriptionId, state)
  66. );
  67. this.connections.set(connectionId, connection);
  68. console.log(`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`);
  69. connection.start();
  70. }
  71. });
  72. // Delete old connections
  73. deletedIds.forEach(id => {
  74. console.log(`[ConnectionManager] Closing connection ${id}`);
  75. const connection = this.connections.get(id);
  76. this.connections.delete(id);
  77. connection.close();
  78. });
  79. }
  80. stateChanged(subscriptionId, state) {
  81. if (this.stateListener) {
  82. try {
  83. this.stateListener(subscriptionId, state);
  84. } catch (e) {
  85. console.error(`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, e);
  86. }
  87. }
  88. }
  89. notificationReceived(subscriptionId, notification) {
  90. if (this.notificationListener) {
  91. try {
  92. this.notificationListener(subscriptionId, notification);
  93. } catch (e) {
  94. console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e);
  95. }
  96. }
  97. }
  98. }
  99. const makeConnectionId = async (subscription, user) => {
  100. return (user)
  101. ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`)
  102. : hashCode(`${subscription.id}`);
  103. }
  104. const connectionManager = new ConnectionManager();
  105. export default connectionManager;