UNPKG

mastodon-api

Version:

Mastodon API library with streaming support

309 lines (261 loc) 12.5 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _request = require('request'); var _request2 = _interopRequireDefault(_request); var _events = require('events'); var _helpers = require('./helpers'); var _helpers2 = _interopRequireDefault(_helpers); var _parser = require('./parser'); var _parser2 = _interopRequireDefault(_parser); var _settings = require('./settings'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var StreamingAPIConnection = function (_EventEmitter) { _inherits(StreamingAPIConnection, _EventEmitter); function StreamingAPIConnection(requestOptions, mastodonOptions) { _classCallCheck(this, StreamingAPIConnection); var _this = _possibleConstructorReturn(this, (StreamingAPIConnection.__proto__ || Object.getPrototypeOf(StreamingAPIConnection)).call(this)); _this.requestOptions = requestOptions; _this.mastodonOptions = mastodonOptions; return _this; } /** * Resets the connection. * - clears request, response, parser * - removes scheduled reconnect handle (if one was scheduled) * - stops the stall abort timeout handle (if one was scheduled) */ _createClass(StreamingAPIConnection, [{ key: '_resetConnection', value: function _resetConnection() { if (this.request) { // clear our reference to the `request` instance this.request.removeAllListeners(); this.request.destroy(); } if (this.response) { // clear our reference to the http.IncomingMessage instance this.response.removeAllListeners(); this.response.destroy(); } if (this.parser) { this.parser.removeAllListeners(); } // ensure a scheduled reconnect does not occur (if one was scheduled) // this can happen if we get a close event before .stop() is called clearTimeout(this._scheduledReconnect); delete this._scheduledReconnect; // clear our stall abort timeout this._stopStallAbortTimeout(); } /** * Resets the parameters used in determining the next reconnect time */ }, { key: '_resetRetryParams', value: function _resetRetryParams() { // delay for next reconnection attempt this._connectInterval = 0; // flag indicating whether we used a 0-delay reconnect this._usedFirstReconnect = false; } }, { key: '_startPersistentConnection', value: function _startPersistentConnection() { var self = this; self._resetConnection(); self._setupParser(); self._resetStallAbortTimeout(); self.request = _request2.default.get(self.requestOptions); self.emit('connect', self.request); self.request.on('response', function (response) { // reset our reconnection attempt flag so next attempt goes through with 0 delay // if we get a transport-level error self._usedFirstReconnect = false; // start a stall abort timeout handle self._resetStallAbortTimeout(); self.response = response; if (_settings.STATUS_CODES_TO_ABORT_ON.includes(response.statusCode)) { var body = ''; self.response.on('data', function (chunk) { body += chunk.toString('utf8'); try { body = JSON.parse(body); } catch (jsonDecodeError) { // if non-JSON text was returned, we'll just attach it to the error as-is } var error = _helpers2.default.makeMastodonError('Bad Streaming API request: ' + self.response.statusCode); error.statusCode = self.response.statusCode; error = _helpers2.default.attachBodyInfoToError(error, body); self.emit('error', error); // stop the stream explicitly so we don't reconnect self.stop(); body = null; }); } else { self.response.on('data', function (chunk) { self._connectInterval = 0; self._resetStallAbortTimeout(); self.parser.parse(chunk.toString('utf8')); }); self.response.on('error', function (err) { // expose response errors on twit instance self.emit('error', err); }); // connected without an error response from Dweet.io, emit `connected` event // this must be emitted after all its event handlers are bound // so the reference to `self.response` is not // interfered-with by the user until it is emitted self.emit('connected', self.response); } }); self.request.on('close', self._onClose.bind(self)); self.request.on('error', function () { self._scheduleReconnect.bind(self); }); return self; } /** * Handle when the request or response closes. * Schedule a reconnect * */ }, { key: '_onClose', value: function _onClose() { var self = this; self._stopStallAbortTimeout(); if (self._scheduledReconnect) { // if we already have a reconnect scheduled, don't schedule another one. // this race condition can happen if the http.ClientRequest // and http.IncomingMessage both emit `close` return; } self._scheduleReconnect(); } /** * Kick off the http request, and persist the connection */ }, { key: 'start', value: function start() { this._resetRetryParams(); this._startPersistentConnection(); return this; } /** * Abort the http request, stop scheduled reconnect (if one was scheduled) and clear state */ }, { key: 'stop', value: function stop() { // clear connection variables and timeout handles this._resetConnection(); this._resetRetryParams(); return this; } /** * Stop and restart the stall abort timer (called when new data is received) * * If we go 90s without receiving data from dweet.io, we abort the request & reconnect. */ }, { key: '_resetStallAbortTimeout', value: function _resetStallAbortTimeout() { var self = this; // stop the previous stall abort timer self._stopStallAbortTimeout(); // start a new 90s timeout to trigger a close & reconnect if no data received self._stallAbortTimeout = setTimeout(function () { self._scheduleReconnect(); }, 90000); return this; } }, { key: '_stopStallAbortTimeout', value: function _stopStallAbortTimeout() { clearTimeout(this._stallAbortTimeout); // mark the timer as `null` so it is clear // via introspection that the timeout is not scheduled delete this._stallAbortTimeout; return this; } /** * Computes the next time a reconnect should occur (based on the last HTTP response received) * and starts a timeout handle to begin reconnecting after `self._connectInterval` passes. * * @return {Undefined} */ }, { key: '_scheduleReconnect', value: function _scheduleReconnect() { var self = this; if (self.response && self.response.statusCode === 420) { // start with a 1 minute wait and double each attempt if (!self._connectInterval) { self._connectInterval = 60000; } else { self._connectInterval *= 2; } } else if (self.response && String(self.response.statusCode).charAt(0) === '5') { // 5xx errors // start with a 5s wait, double each attempt up to 320s if (!self._connectInterval) { self._connectInterval = 5000; } else if (self._connectInterval < 320000) { self._connectInterval *= 2; } else { self._connectInterval = 320000; } } else { // we did not get an HTTP response from our last connection attempt. // DNS/TCP error, or a stall in the stream (and stall timer closed the connection) // eslint-disable-next-line no-lonely-if if (!self._usedFirstReconnect) { // first reconnection attempt on a valid connection should occur immediately self._connectInterval = 0; self._usedFirstReconnect = true; } else if (self._connectInterval < 16000) { // linearly increase delay by 250ms up to 16s self._connectInterval += 250; } else { // cap out reconnect interval at 16s self._connectInterval = 16000; } } // schedule the reconnect self._scheduledReconnect = setTimeout(function () { self._startPersistentConnection(); }, self._connectInterval); self.emit('reconnect', self.request, self.response, self._connectInterval); } }, { key: '_setupParser', value: function _setupParser() { var self = this; self.parser = new _parser2.default(); self.parser.on('element', function (msg) { self.emit('message', msg); }); self.parser.on('error', function (err) { self.emit('parser-error', err); }); self.parser.on('connection-limit-exceeded', function (err) { self.emit('error', err); }); } }, { key: '_handleDisconnect', value: function _handleDisconnect(msg) { this.emit('disconnect', msg); this.stop(); } }]); return StreamingAPIConnection; }(_events.EventEmitter); exports.default = StreamingAPIConnection;