Source: components/state-lookup.js

  1. "use strict";
  2. var Promise = require("bluebird");
  3. /**
  4. * Construct a new state lookup entity.
  5. *
  6. * This component stores state events for specific event types which can be
  7. * queried at a later date. This component will perform network requests to
  8. * fetch the current state for a given room ID. It relies on
  9. * {@link StateLookup#onEvent} being called with later events in order to
  10. * stay up-to-date. This should be connected to the <code>onEvent</code>
  11. * handler on the {@link Bridge}.
  12. * @constructor
  13. * @param {Object} opts Options for this constructor
  14. * @param {MatrixClient} opts.client Required. The client which will perform
  15. * /state requests.
  16. * @param {string[]} opts.eventTypes The state event types to track.
  17. * @throws if there is no client.
  18. */
  19. function StateLookup(opts) {
  20. if (!opts.client) {
  21. throw new Error("client property must be supplied");
  22. }
  23. this._client = opts.client;
  24. this._eventTypes = {}; // store it as a map
  25. var self = this;
  26. (opts.eventTypes || []).forEach(function(t) {
  27. self._eventTypes[t] = true;
  28. });
  29. this._dict = {
  30. // $room_id: {
  31. // syncPromise: Promise,
  32. // events: {
  33. // $event_type: {
  34. // $state_key: { Event }
  35. // }
  36. // }
  37. // }
  38. };
  39. }
  40. /**
  41. * Get a stored state event.
  42. * @param {string} roomId
  43. * @param {string} eventType
  44. * @param {string=} stateKey If specified, this function will return either
  45. * the event or null. If not specified, this function will always return an
  46. * array of events, which may be empty.
  47. * @return {?Object|Object[]}
  48. */
  49. StateLookup.prototype.getState = function(roomId, eventType, stateKey) {
  50. var stateKeySpecified = (stateKey !== undefined && stateKey !== null);
  51. var r = this._dict[roomId];
  52. if (!r) {
  53. return stateKeySpecified ? null : [];
  54. }
  55. var es = r.events;
  56. if (!es[eventType]) {
  57. return stateKeySpecified ? null : [];
  58. }
  59. if (stateKeySpecified) {
  60. return es[eventType][stateKey] || null;
  61. }
  62. return Object.keys(es[eventType]).map(function(skey) {
  63. return es[eventType][skey];
  64. });
  65. };
  66. /**
  67. * Track a given room. The client must have access to this room.
  68. *
  69. * This will perform a room state query initially. Subsequent calls will do
  70. * nothing, as it will rely on events being pushed to it via {@link StateLookup#onEvent}.
  71. *
  72. * @param {string} roomId The room ID to start tracking. You can track multiple
  73. * rooms by calling this function multiple times with different room IDs.
  74. * @return {Promise} Resolves when the room is being tracked. Rejects if the room
  75. * cannot be tracked.
  76. */
  77. StateLookup.prototype.trackRoom = function(roomId) {
  78. var r = this._dict[roomId] = this._dict[roomId] || {};
  79. if (r.syncPromise) {
  80. return r.syncPromise;
  81. }
  82. var self = this;
  83. r.events = {};
  84. r.syncPromise = new Promise(function(resolve, reject) {
  85. // convoluted query function so we can do retries on errors
  86. var queryRoomState = function() {
  87. self._client.roomState(roomId).then(function(events) {
  88. events.forEach(function(ev) {
  89. if (self._eventTypes[ev.type]) {
  90. if (!r.events[ev.type]) {
  91. r.events[ev.type] = {};
  92. }
  93. r.events[ev.type][ev.state_key] = ev;
  94. }
  95. });
  96. resolve(r);
  97. }, function(err) {
  98. if (err.httpStatus >= 400 && err.httpStatus < 600) { // 4xx, 5xx
  99. reject(err); // don't have permission, don't retry.
  100. }
  101. // wait a bit then try again
  102. Promise.delay(3000).then(function() {
  103. if (!self._dict[roomId]) {
  104. return;
  105. }
  106. queryRoomState();
  107. });
  108. });
  109. };
  110. queryRoomState();
  111. });
  112. return r.syncPromise;
  113. };
  114. /**
  115. * Stop tracking a given room.
  116. *
  117. * This will stop further tracking of state events in the given room and delete
  118. * existing stored state for it.
  119. *
  120. * @param {string} roomId The room ID to stop tracking.
  121. */
  122. StateLookup.prototype.untrackRoom = function(roomId) {
  123. delete this._dict[roomId];
  124. };
  125. /**
  126. * Update any state dictionaries with this event. If there is nothing tracking
  127. * this room, nothing is stored.
  128. * @param {Object} event Raw matrix event
  129. */
  130. StateLookup.prototype.onEvent = function(event) {
  131. if (!this._dict[event.room_id]) {
  132. return;
  133. }
  134. var r = this._dict[event.room_id];
  135. if (r.syncPromise.isPending()) {
  136. // well this is awkward. We're being pushed events whilst we have
  137. // a /state request ongoing. We always expect to be notified of the
  138. // latest state via push, so if we ignore the /state response for this
  139. // event and always use the pushed events we should remain in sync.
  140. // we'll add our own listener for the sync promise and then update this
  141. // value.
  142. r.syncPromise.then(function(r_) {
  143. // We get 'r' passed back in from the resolve() call so we don't
  144. // have to capture it here; thus avoiding a memory reference cycle
  145. if (!r_.events[event.type]) {
  146. r_.events[event.type] = {};
  147. }
  148. r_.events[event.type][event.state_key] = event;
  149. });
  150. return;
  151. }
  152. // blunt update
  153. if (!r.events[event.type]) {
  154. r.events[event.type] = {};
  155. }
  156. r.events[event.type][event.state_key] = event;
  157. };
  158. module.exports = StateLookup;