sse.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /**
  2. * Copyright (C) 2016 Maxime Petazzoni <maxime.petazzoni@bulix.org>.
  3. * All rights reserved.
  4. */
  5. var SSE = function (url, options) {
  6. if (!(this instanceof SSE)) {
  7. return new SSE(url, options);
  8. }
  9. this.INITIALIZING = -1;
  10. this.CONNECTING = 0;
  11. this.OPEN = 1;
  12. this.CLOSED = 2;
  13. this.url = url;
  14. options = options || {};
  15. this.headers = options.headers || {};
  16. this.payload = options.payload !== undefined ? options.payload : '';
  17. this.method = options.method || (this.payload && 'POST' || 'GET');
  18. this.FIELD_SEPARATOR = ':';
  19. this.listeners = {};
  20. this.xhr = null;
  21. this.readyState = this.INITIALIZING;
  22. this.progress = 0;
  23. this.chunk = '';
  24. this.addEventListener = function(type, listener) {
  25. if (this.listeners[type] === undefined) {
  26. this.listeners[type] = [];
  27. }
  28. if (this.listeners[type].indexOf(listener) === -1) {
  29. this.listeners[type].push(listener);
  30. }
  31. };
  32. this.removeEventListener = function(type, listener) {
  33. if (this.listeners[type] === undefined) {
  34. return;
  35. }
  36. var filtered = [];
  37. this.listeners[type].forEach(function(element) {
  38. if (element !== listener) {
  39. filtered.push(element);
  40. }
  41. });
  42. if (filtered.length === 0) {
  43. delete this.listeners[type];
  44. } else {
  45. this.listeners[type] = filtered;
  46. }
  47. };
  48. this.dispatchEvent = function(e) {
  49. if (!e) {
  50. return true;
  51. }
  52. e.source = this;
  53. var onHandler = 'on' + e.type;
  54. if (this.hasOwnProperty(onHandler)) {
  55. this[onHandler].call(this, e);
  56. if (e.defaultPrevented) {
  57. return false;
  58. }
  59. }
  60. if (this.listeners[e.type]) {
  61. return this.listeners[e.type].every(function(callback) {
  62. callback(e);
  63. return !e.defaultPrevented;
  64. });
  65. }
  66. return true;
  67. };
  68. this._setReadyState = function (state) {
  69. var event = new CustomEvent('readystatechange');
  70. event.readyState = state;
  71. this.readyState = state;
  72. this.dispatchEvent(event);
  73. };
  74. this._onStreamFailure = function(e) {
  75. this.dispatchEvent(new CustomEvent('error'));
  76. this.close();
  77. }
  78. this._onStreamProgress = function(e) {
  79. if (this.xhr.status !== 200 && this.readyState !== this.CLOSED) {
  80. this._onStreamFailure(e);
  81. return;
  82. }
  83. if (this.readyState == this.CONNECTING) {
  84. this.dispatchEvent(new CustomEvent('open'));
  85. this._setReadyState(this.OPEN);
  86. }
  87. var data = this.xhr.responseText.substring(this.progress);
  88. this.progress += data.length;
  89. data.split(/(\r\n|\r|\n){2}/g).forEach(function(part) {
  90. if (part.trim().length === 0) {
  91. this.dispatchEvent(this._parseEventChunk(this.chunk.trim()));
  92. this.chunk = '';
  93. } else {
  94. this.chunk += part;
  95. }
  96. }.bind(this));
  97. };
  98. this._onStreamLoaded = function(e) {
  99. this._onStreamProgress(e);
  100. // Parse the last chunk.
  101. this.dispatchEvent(this._parseEventChunk(this.chunk));
  102. this.chunk = '';
  103. };
  104. /**
  105. * Parse a received SSE event chunk into a constructed event object.
  106. */
  107. this._parseEventChunk = function(chunk) {
  108. if (!chunk || chunk.length === 0) {
  109. return null;
  110. }
  111. var e = {'id': null, 'retry': null, 'data': '', 'event': 'message'};
  112. chunk.split(/\n|\r\n|\r/).forEach(function(line) {
  113. line = line.trimRight();
  114. var index = line.indexOf(this.FIELD_SEPARATOR);
  115. if (index <= 0) {
  116. // Line was either empty, or started with a separator and is a comment.
  117. // Either way, ignore.
  118. return;
  119. }
  120. var field = line.substring(0, index);
  121. if (!(field in e)) {
  122. return;
  123. }
  124. var value = line.substring(index + 1).trimLeft();
  125. if (field === 'data') {
  126. e[field] += value;
  127. } else {
  128. e[field] = value;
  129. }
  130. }.bind(this));
  131. var event = new CustomEvent(e.event);
  132. event.data = e.data;
  133. event.id = e.id;
  134. return event;
  135. };
  136. this._checkStreamClosed = function() {
  137. if (this.xhr.readyState === XMLHttpRequest.DONE) {
  138. this._setReadyState(this.CLOSED);
  139. }
  140. };
  141. this.stream = function() {
  142. this._setReadyState(this.CONNECTING);
  143. this.xhr = new XMLHttpRequest();
  144. this.xhr.addEventListener('progress', this._onStreamProgress.bind(this));
  145. this.xhr.addEventListener('load', this._onStreamLoaded.bind(this));
  146. this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this));
  147. this.xhr.addEventListener('error', this._onStreamFailure.bind(this));
  148. this.xhr.addEventListener('abort', this._onStreamFailure.bind(this));
  149. this.xhr.open(this.method, this.url);
  150. for (var header in this.headers) {
  151. this.xhr.setRequestHeader(header, this.headers[header]);
  152. }
  153. this.xhr.send(this.payload);
  154. };
  155. this.close = function() {
  156. if (this.readyState === this.CLOSED) {
  157. return;
  158. }
  159. this.xhr.abort();
  160. this.xhr = null;
  161. this._setReadyState(this.CLOSED);
  162. };
  163. };
  164. // Export our SSE module for npm.js
  165. if (typeof exports !== 'undefined') {
  166. exports.SSE = SSE;
  167. }