utils.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import _converse from '../../shared/_converse.js';
  2. import api, { converse } from '../../shared/api/index.js';
  3. import log from '../../log.js';
  4. import { getOpenPromise } from '@converse/openpromise';
  5. import { isTestEnv } from '../../utils/session.js';
  6. const { Strophe } = converse.env;
  7. const u = converse.env.utils;
  8. function isStreamManagementSupported () {
  9. if (api.connection.isType('bosh') && !isTestEnv()) {
  10. return false;
  11. }
  12. return api.disco.stream.getFeature('sm', Strophe.NS.SM);
  13. }
  14. function handleAck (el) {
  15. if (!_converse.session.get('smacks_enabled')) {
  16. return true;
  17. }
  18. const handled = parseInt(el.getAttribute('h'), 10);
  19. const last_known_handled = _converse.session.get('num_stanzas_handled_by_server');
  20. const delta = handled - last_known_handled;
  21. if (delta < 0) {
  22. const err_msg =
  23. `New reported stanza count lower than previous. ` + `New: ${handled} - Previous: ${last_known_handled}`;
  24. log.error(err_msg);
  25. }
  26. const unacked_stanzas = _converse.session.get('unacked_stanzas');
  27. if (delta > unacked_stanzas.length) {
  28. const err_msg =
  29. `Higher reported acknowledge count than unacknowledged stanzas. ` +
  30. `Reported Acknowledged Count: ${delta} -` +
  31. `Unacknowledged Stanza Count: ${unacked_stanzas.length} -` +
  32. `New: ${handled} - Previous: ${last_known_handled}`;
  33. log.error(err_msg);
  34. }
  35. _converse.session.save({
  36. 'num_stanzas_handled_by_server': handled,
  37. 'num_stanzas_since_last_ack': 0,
  38. 'unacked_stanzas': unacked_stanzas.slice(delta)
  39. });
  40. return true;
  41. }
  42. function sendAck () {
  43. if (_converse.session.get('smacks_enabled')) {
  44. const h = _converse.session.get('num_stanzas_handled');
  45. const stanza = u.toStanza(`<a xmlns="${Strophe.NS.SM}" h="${h}"/>`);
  46. api.send(stanza);
  47. }
  48. return true;
  49. }
  50. function stanzaHandler (el) {
  51. if (_converse.session.get('smacks_enabled')) {
  52. if (u.isTagEqual(el, 'iq') || u.isTagEqual(el, 'presence') || u.isTagEqual(el, 'message')) {
  53. const h = _converse.session.get('num_stanzas_handled');
  54. _converse.session.save('num_stanzas_handled', h + 1);
  55. }
  56. }
  57. return true;
  58. }
  59. export function initSessionData () {
  60. _converse.session.save({
  61. 'smacks_enabled': _converse.session.get('smacks_enabled') || false,
  62. 'num_stanzas_handled': _converse.session.get('num_stanzas_handled') || 0,
  63. 'num_stanzas_handled_by_server': _converse.session.get('num_stanzas_handled_by_server') || 0,
  64. 'num_stanzas_since_last_ack': _converse.session.get('num_stanzas_since_last_ack') || 0,
  65. 'unacked_stanzas': _converse.session.get('unacked_stanzas') || []
  66. });
  67. }
  68. function resetSessionData () {
  69. _converse.session?.save({
  70. 'smacks_enabled': false,
  71. 'num_stanzas_handled': 0,
  72. 'num_stanzas_handled_by_server': 0,
  73. 'num_stanzas_since_last_ack': 0,
  74. 'unacked_stanzas': []
  75. });
  76. }
  77. function saveSessionData (el) {
  78. const data = { 'smacks_enabled': true };
  79. if (['1', 'true'].includes(el.getAttribute('resume'))) {
  80. data['smacks_stream_id'] = el.getAttribute('id');
  81. }
  82. _converse.session.save(data);
  83. return true;
  84. }
  85. function onFailedStanza (el) {
  86. if (el.querySelector('item-not-found')) {
  87. // Stream resumption must happen before resource binding but
  88. // enabling a new stream must happen after resource binding.
  89. // Since resumption failed, we simply continue.
  90. //
  91. // After resource binding, sendEnableStanza will be called
  92. // based on the afterResourceBinding event.
  93. log.warn(
  94. 'Could not resume previous SMACKS session, session id not found. ' + 'A new session will be established.'
  95. );
  96. } else {
  97. log.error('Failed to enable stream management');
  98. log.error(el.outerHTML);
  99. }
  100. resetSessionData();
  101. /**
  102. * Triggered when the XEP-0198 stream could not be resumed.
  103. * @event _converse#streamResumptionFailed
  104. */
  105. api.trigger('streamResumptionFailed');
  106. return true;
  107. }
  108. function resendUnackedStanzas () {
  109. const stanzas = _converse.session.get('unacked_stanzas');
  110. // We clear the unacked_stanzas array because it'll get populated
  111. // again in `onStanzaSent`
  112. _converse.session.save('unacked_stanzas', []);
  113. // XXX: Currently we're resending *all* unacked stanzas, including
  114. // IQ[type="get"] stanzas that longer have handlers (because the
  115. // page reloaded or we reconnected, causing removal of handlers).
  116. //
  117. // *Side-note:* Is it necessary to clear handlers upon reconnection?
  118. //
  119. // I've considered not resending those stanzas, but then keeping
  120. // track of what's been sent and ack'd and their order gets
  121. // prohibitively complex.
  122. //
  123. // It's unclear how much of a problem this poses.
  124. //
  125. // Two possible solutions are running @converse/headless as a
  126. // service worker or handling IQ[type="result"] stanzas
  127. // differently, more like push stanzas, so that they don't need
  128. // explicit handlers.
  129. stanzas.forEach(s => api.send(s));
  130. }
  131. function onResumedStanza (el) {
  132. saveSessionData(el);
  133. handleAck(el);
  134. resendUnackedStanzas();
  135. _converse.connection.do_bind = false; // No need to bind our resource anymore
  136. _converse.connection.authenticated = true;
  137. _converse.connection.restored = true;
  138. _converse.connection._changeConnectStatus(Strophe.Status.CONNECTED, null);
  139. }
  140. async function sendResumeStanza () {
  141. const promise = getOpenPromise();
  142. _converse.connection._addSysHandler(el => promise.resolve(onResumedStanza(el)), Strophe.NS.SM, 'resumed');
  143. _converse.connection._addSysHandler(el => promise.resolve(onFailedStanza(el)), Strophe.NS.SM, 'failed');
  144. const previous_id = _converse.session.get('smacks_stream_id');
  145. const h = _converse.session.get('num_stanzas_handled');
  146. const stanza = u.toStanza(`<resume xmlns="${Strophe.NS.SM}" h="${h}" previd="${previous_id}"/>`);
  147. api.send(stanza);
  148. _converse.connection.flush();
  149. await promise;
  150. }
  151. export async function sendEnableStanza () {
  152. if (!api.settings.get('enable_smacks') || _converse.session.get('smacks_enabled')) {
  153. return;
  154. }
  155. if (await isStreamManagementSupported()) {
  156. const promise = getOpenPromise();
  157. _converse.connection._addSysHandler(el => promise.resolve(saveSessionData(el)), Strophe.NS.SM, 'enabled');
  158. _converse.connection._addSysHandler(el => promise.resolve(onFailedStanza(el)), Strophe.NS.SM, 'failed');
  159. const resume = api.connection.isType('websocket') || isTestEnv();
  160. const stanza = u.toStanza(`<enable xmlns="${Strophe.NS.SM}" resume="${resume}"/>`);
  161. api.send(stanza);
  162. _converse.connection.flush();
  163. await promise;
  164. }
  165. }
  166. const smacks_handlers = [];
  167. export async function enableStreamManagement () {
  168. if (!api.settings.get('enable_smacks')) {
  169. return;
  170. }
  171. if (!(await isStreamManagementSupported())) {
  172. return;
  173. }
  174. const conn = _converse.connection;
  175. while (smacks_handlers.length) {
  176. conn.deleteHandler(smacks_handlers.pop());
  177. }
  178. smacks_handlers.push(conn.addHandler(stanzaHandler));
  179. smacks_handlers.push(conn.addHandler(sendAck, Strophe.NS.SM, 'r'));
  180. smacks_handlers.push(conn.addHandler(handleAck, Strophe.NS.SM, 'a'));
  181. if (_converse.session?.get('smacks_stream_id')) {
  182. await sendResumeStanza();
  183. } else {
  184. resetSessionData();
  185. }
  186. }
  187. export function onStanzaSent (stanza) {
  188. if (!_converse.session) {
  189. log.warn('No _converse.session!');
  190. return;
  191. }
  192. if (!_converse.session.get('smacks_enabled')) {
  193. return;
  194. }
  195. if (u.isTagEqual(stanza, 'iq') || u.isTagEqual(stanza, 'presence') || u.isTagEqual(stanza, 'message')) {
  196. const stanza_string = Strophe.serialize(stanza);
  197. _converse.session.save(
  198. 'unacked_stanzas',
  199. (_converse.session.get('unacked_stanzas') || []).concat([stanza_string])
  200. );
  201. const max_unacked = api.settings.get('smacks_max_unacked_stanzas');
  202. if (max_unacked > 0) {
  203. const num = _converse.session.get('num_stanzas_since_last_ack') + 1;
  204. if (num % max_unacked === 0) {
  205. // Request confirmation of sent stanzas
  206. api.send(u.toStanza(`<r xmlns="${Strophe.NS.SM}"/>`));
  207. }
  208. _converse.session.save({ 'num_stanzas_since_last_ack': num });
  209. }
  210. }
  211. }