├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── examples ├── keys.json.template ├── site.basic.js ├── statuses.filter.basic.js ├── statuses.sample.basic.js └── user.basic.js ├── lib ├── connection.js ├── helpers.js ├── main.js └── parser.js ├── package.json └── test ├── connection.js ├── helpers.js └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules/**/* 3 | .DS_Store 4 | tmp/**/* 5 | .idea 6 | .idea/**/* 7 | *.iml 8 | *.log 9 | examples/keys.json 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | examples/keys.json 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "6" 6 | - "4" 7 | 8 | script: 9 | - npm test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twitter-stream-api 2 | 3 | [![Dependencies](https://img.shields.io/david/trygve-lie/twitter-stream-api.svg?style=flat-square)](https://david-dm.org/trygve-lie/twitter-stream-api)[![Build Status](http://img.shields.io/travis/trygve-lie/twitter-stream-api/master.svg?style=flat-square)](https://travis-ci.org/trygve-lie/twitter-stream-api) 4 | 5 | 6 | A streaming [Twitter Stream API](https://dev.twitter.com/streaming/overview) 7 | client with extended exposure of the underlaying protocol events. It does also 8 | fully adhere to Twitters different reconnect rules. 9 | 10 | 11 | 12 | ## Installation 13 | 14 | ```bash 15 | $ npm install twitter-stream-api 16 | ``` 17 | 18 | 19 | ## Simple stream usage 20 | 21 | Connect to the Twitter stream API and listen for messages containing the word 22 | "javascript". 23 | 24 | ```js 25 | var TwitterStream = require('twitter-stream-api'), 26 | fs = require('fs'); 27 | 28 | var keys = { 29 | consumer_key : "your_consumer_key", 30 | consumer_secret : "your_consumer_secret", 31 | token : "your_access_token_key", 32 | token_secret : "your_access_token_secret" 33 | }; 34 | 35 | var Twitter = new TwitterStream(keys, false); 36 | Twitter.stream('statuses/filter', { 37 | track: 'javascript' 38 | }); 39 | 40 | Twitter.pipe(fs.createWriteStream('tweets.json')); 41 | ``` 42 | 43 | 44 | 45 | ## Constructor 46 | 47 | Create a new Twitter Stream API instance. 48 | 49 | ```js 50 | var Twitter = new TwitterStream(keys, [objectMode, options]); 51 | ``` 52 | 53 | 54 | ### keys (required) 55 | 56 | Takes an Object containing your Twitter API keys and access tokens. The Object 57 | are as follow: 58 | 59 | ```js 60 | { 61 | consumer_key : "your_consumer_key", 62 | consumer_secret : "your_consumer_secret", 63 | token : "your_access_token_key", 64 | token_secret : "your_access_token_secret" 65 | } 66 | ``` 67 | 68 | Twitter API keys and tokens can be [generated here](https://apps.twitter.com/). 69 | 70 | 71 | ### objectMode (optional) 72 | 73 | Boolean value for controlling if the stream should emit Objects or not. Default 74 | value is `true` which set the stream to emit Objects. If a non-object stream is 75 | wanted, set the value to `false`. 76 | 77 | 78 | ### options (optional) 79 | 80 | An Object containing misc configuration. The following values can be provided: 81 | 82 | * gzip - Boolean value for enabling / disabling gzip on the connection against Twitter. 83 | * pool - Sets pool configuration on the underlaying request.js object. 84 | 85 | Please refere to [request.js](https://github.com/request/request) for further 86 | documentation on these cunfiguration options. 87 | 88 | 89 | 90 | ## API 91 | 92 | The Twitter Stream API instance have the following API: 93 | 94 | 95 | ### .stream(endpoint, parameters) 96 | 97 | Opens a connection to a given stream endpoint. 98 | 99 | 100 | #### endpoint (required) 101 | 102 | The following values can be provided: 103 | 104 | * `statuses/filter` [API Doc](https://dev.twitter.com/streaming/reference/post/statuses/filter) 105 | * `statuses/sample` [API Doc](https://dev.twitter.com/streaming/reference/get/statuses/sample) 106 | * `statuses/firehose` [API Doc](https://dev.twitter.com/streaming/reference/get/statuses/firehose) 107 | * `user` [API Doc](https://dev.twitter.com/streaming/reference/get/user) 108 | * `site` [API Doc](https://dev.twitter.com/streaming/reference/get/site) 109 | 110 | 111 | #### parameters (required) 112 | 113 | Object holding optional Twitter Stream API endpoint parameters. The Twitter 114 | Stream API endpoints can take a set of given parameters which can be found in 115 | the API documentation for each endpoint. 116 | 117 | Example: 118 | 119 | The `statuses/filter` endpoint can take a [`track`](https://dev.twitter.com/streaming/reference/post/statuses/filter) 120 | parameter for tracking tweets on keywords. The same endpoint can also take a 121 | `stall_warnings` parameter to include stall warnings in the Twitter stream. 122 | 123 | To track the keyword `javascript` and include stall warnings, do as follow: 124 | 125 | ```js 126 | Twitter.stream('statuses/filter', { 127 | track: 'javascript', 128 | stall_warnings: true 129 | }); 130 | ``` 131 | 132 | Do note that the `track` and `follow` parameters can take both a comma separated 133 | list of values or an Array of values. 134 | 135 | ```js 136 | Twitter.stream('statuses/filter', { 137 | track: 'javascript,rust' 138 | }); 139 | ``` 140 | 141 | is the same as: 142 | 143 | ```js 144 | Twitter.stream('statuses/filter', { 145 | track: ['javascript','rust'] 146 | }); 147 | ``` 148 | 149 | 150 | ### .close() 151 | 152 | Closes the connection against the Twitter Stream API. 153 | 154 | 155 | ### .debug(callback) 156 | 157 | Under the hood this client use [request](https://github.com/request/request) to 158 | connect to the Twitter Stream API. Request have several tools for debugging its 159 | connection(s). This method provide access to the underlaying request object so 160 | one can plug in a debugger to [request](https://github.com/request/request). 161 | 162 | The underlaying request object are available as the first argument on the 163 | callback. 164 | 165 | Example using [request-debug](https://github.com/request/request-debug): 166 | 167 | ```js 168 | var Twitter = new TwitterStream(keys); 169 | 170 | Twitter.debug(function (reqObj) { 171 | require('request-debug')(reqObj, function (type, data, req) { 172 | console.log(type, data, req); 173 | }); 174 | }); 175 | ``` 176 | 177 | 178 | 179 | ## Events 180 | 181 | twitter-stream-api expose a rich set of events making it possible to monitor and 182 | take action upon what is going on under the hood. 183 | 184 | 185 | ### connection success 186 | 187 | Emitted when a successfull connection to the Twitter Stream API are established. 188 | 189 | ```js 190 | Twitter.on('connection success', function (uri) { 191 | console.log('connection success', uri); 192 | }); 193 | ``` 194 | 195 | 196 | ### connection aborted 197 | 198 | Emitted when a the connection to the Twitter Stream API are taken down / closed. 199 | 200 | ```js 201 | Twitter.on('connection aborted', function () { 202 | console.log('connection aborted'); 203 | }); 204 | ``` 205 | 206 | 207 | ### connection error network 208 | 209 | Emitted when the connection to the Twitter Stream API have TCP/IP level network 210 | errors. This error event are normally emitted if there are network level errors 211 | during the connection process. 212 | 213 | ```js 214 | Twitter.on('connection error network', function (error) { 215 | console.log('connection error network', error); 216 | }); 217 | ``` 218 | 219 | When this event is emitted a linear reconnect will start. The reconnect will 220 | attempt a reconnect after 250 milliseconds and increase the reconnect attempts 221 | linearly up to 16 seconds. 222 | 223 | 224 | ### connection error stall 225 | 226 | Emitted when the connection to the Twitter Stream API have been flagged as stall. 227 | A stall connection is a connection which have not received any new data or keep 228 | alive messages from the Twitter Stream API during a period of 90 seconds. 229 | 230 | This error event are normally emitted when a connection have been established 231 | but there has been a drop in it after a while. 232 | 233 | ```js 234 | Twitter.on('connection error stall', function () { 235 | console.log('connection error stall'); 236 | }); 237 | ``` 238 | 239 | When this event is emitted a linear reconnect will start. The reconnect will 240 | attempt a reconnect after 250 milliseconds and increase the reconnect attempts 241 | linearly up to 16 seconds. 242 | 243 | 244 | ### connection error http 245 | 246 | Emitted when the connection to the Twitter Stream API return an HTTP error code. 247 | 248 | This error event are normally emitted if there are HTTP errors during the 249 | connection process. 250 | 251 | ```js 252 | Twitter.on('connection error http', function (httpStatusCode) { 253 | console.log('connection error http', httpStatusCode); 254 | }); 255 | ``` 256 | 257 | When this event is emitted a exponentially reconnect will start. The reconnect 258 | will attempt a reconnect after 5 seconds and increase the reconnect attempts 259 | exponentially up to 320 seconds. 260 | 261 | 262 | ### connection rate limit 263 | 264 | Emitted when the connection to the Twitter Stream API are being rate limited. 265 | Twitter does only allow one connection for each application to its Stream API.Multiple connections or to rappid reconnects will cause a rate limiting to 266 | happen. 267 | 268 | ```js 269 | Twitter.on('connection rate limit', function (httpStatusCode) { 270 | console.log('connection rate limit', httpStatusCode); 271 | }); 272 | ``` 273 | 274 | When this event is emitted a exponentially reconnect will start. The reconnect 275 | will attempt a reconnect after 1 minute and double the reconnect attempts 276 | exponentially. 277 | 278 | 279 | ### connection error unknown 280 | 281 | Emitted when the connection to the Twitter Stream API throw an unexpected error 282 | which are not within the errors defined by the Twitter Stream API documentation. 283 | 284 | ```js 285 | Twitter.on('connection error unknown', function (error) { 286 | console.log('connection error unknown', error); 287 | Twitter.close(); 288 | }); 289 | ``` 290 | 291 | When this event is emitted the client will, if it can, keep the connection to 292 | the Twitter Stream API and not attemt to reconnect. Closing the connection 293 | and handling a possilbe reconnect must be handled by the consumer of the client. 294 | 295 | 296 | ### data 297 | 298 | Emitted when a Tweet ocur in the stream. 299 | 300 | ```js 301 | Twitter.on('data', function (obj) { 302 | console.log('data', obj); 303 | }); 304 | ``` 305 | 306 | 307 | ### data keep-alive 308 | 309 | Emitted when the client receive a keep alive message from the Twitter Stream API. 310 | The Twitter Stream API sends a keep alive message every 30 second if no messages 311 | have been sendt to ensure that the connection are kept open. This keep alive 312 | messages are mostly being used under the hood to detect stalled connections and 313 | other connection issues. 314 | 315 | ```js 316 | Twitter.on('data keep-alive', function () { 317 | console.log('data keep-alive'); 318 | }); 319 | ``` 320 | 321 | 322 | ### data error 323 | 324 | Emitted if the client received an message from the Twitter Stream API which the 325 | client could not parse into an object or handle in some other way. 326 | 327 | ```js 328 | Twitter.on('data error', function (error) { 329 | console.log('data error', error); 330 | }); 331 | ``` 332 | 333 | 334 | 335 | ## License 336 | 337 | The MIT License (MIT) 338 | 339 | Copyright (c) 2015 - Trygve Lie - post@trygve-lie.com 340 | 341 | Permission is hereby granted, free of charge, to any person obtaining a copy 342 | of this software and associated documentation files (the "Software"), to deal 343 | in the Software without restriction, including without limitation the rights 344 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 345 | copies of the Software, and to permit persons to whom the Software is 346 | furnished to do so, subject to the following conditions: 347 | 348 | The above copyright notice and this permission notice shall be included in 349 | all copies or substantial portions of the Software. 350 | 351 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 352 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 353 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 354 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 355 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 356 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 357 | THE SOFTWARE. 358 | -------------------------------------------------------------------------------- /examples/keys.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "consumer_key": "", 3 | "consumer_secret": "", 4 | "token": "", 5 | "token_secret": "" 6 | } 7 | -------------------------------------------------------------------------------- /examples/site.basic.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | 6 | var Writable = require('stream').Writable, 7 | TwitterStream = require('../'), 8 | fs = require('fs'), 9 | keysString = fs.readFileSync('./keys.json'), 10 | keys = JSON.parse(keysString); 11 | 12 | 13 | 14 | // Helper for outputting stream to console 15 | 16 | var Output = Writable({objectMode: true}); 17 | Output._write = function (obj, enc, next) { 18 | console.log(obj.id, obj.text); 19 | next(); 20 | }; 21 | 22 | 23 | 24 | var Twitter = new TwitterStream(keys); 25 | 26 | Twitter.stream('site', { 27 | follow: ['2840926455', '65706552'] 28 | }); 29 | 30 | Twitter.on('connection success', function (uri) { 31 | console.log('connection success', uri); 32 | }); 33 | 34 | Twitter.on('connection aborted', function () { 35 | console.log('connection aborted'); 36 | }); 37 | 38 | Twitter.on('connection error network', function () { 39 | console.log('connection error network'); 40 | }); 41 | 42 | Twitter.on('connection error stall', function () { 43 | console.log('connection error stall'); 44 | }); 45 | 46 | Twitter.on('connection error http', function (err) { 47 | console.log('connection error http', err); 48 | }); 49 | 50 | Twitter.on('connection rate limit', function () { 51 | console.log('connection rate limit'); 52 | }); 53 | 54 | Twitter.on('connection error unknown', function () { 55 | console.log('connection error unknown'); 56 | }); 57 | 58 | Twitter.on('data keep-alive', function () { 59 | console.log('data keep-alive'); 60 | }); 61 | 62 | Twitter.on('data error', function () { 63 | console.log('data error'); 64 | }); 65 | 66 | Twitter.pipe(Output); 67 | -------------------------------------------------------------------------------- /examples/statuses.filter.basic.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | 6 | var Writable = require('stream').Writable, 7 | TwitterStream = require('../'), 8 | fs = require('fs'), 9 | keysString = fs.readFileSync('./keys.json'), 10 | keys = JSON.parse(keysString); 11 | 12 | 13 | 14 | // Helper for outputting stream to console 15 | 16 | var Output = Writable({objectMode: true}); 17 | Output._write = function (obj, enc, next) { 18 | console.log(obj.id, obj.text); 19 | next(); 20 | }; 21 | 22 | 23 | 24 | var Twitter = new TwitterStream(keys); 25 | 26 | Twitter.debug(function (reqObj) { 27 | require('request-debug')(reqObj, function (type, data, req) { 28 | console.log('type', type); 29 | }); 30 | }); 31 | 32 | Twitter.stream('statuses/filter', { 33 | follow: ['2840926455', '65706552'], 34 | track: ['javascript'] 35 | }); 36 | 37 | Twitter.on('connection success', function (uri) { 38 | console.log('connection success', uri); 39 | }); 40 | 41 | Twitter.on('connection aborted', function () { 42 | console.log('connection aborted'); 43 | }); 44 | 45 | Twitter.on('connection error network', function () { 46 | console.log('connection error network'); 47 | }); 48 | 49 | Twitter.on('connection error stall', function () { 50 | console.log('connection error stall'); 51 | }); 52 | 53 | Twitter.on('connection error http', function () { 54 | console.log('connection error http'); 55 | }); 56 | 57 | Twitter.on('connection rate limit', function () { 58 | console.log('connection rate limit'); 59 | }); 60 | 61 | Twitter.on('connection error unknown', function () { 62 | console.log('connection error unknown'); 63 | }); 64 | 65 | Twitter.on('data keep-alive', function () { 66 | console.log('data keep-alive'); 67 | }); 68 | 69 | Twitter.on('data error', function () { 70 | console.log('data error'); 71 | }); 72 | 73 | Twitter.pipe(Output); 74 | -------------------------------------------------------------------------------- /examples/statuses.sample.basic.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | 6 | var Writable = require('stream').Writable, 7 | TwitterStream = require('../'), 8 | fs = require('fs'), 9 | keysString = fs.readFileSync('./keys.json'), 10 | keys = JSON.parse(keysString); 11 | 12 | 13 | 14 | // Helper for outputting stream to console 15 | 16 | var Output = Writable({objectMode: true}); 17 | Output._write = function (obj, enc, next) { 18 | console.log(obj.id, obj.text); 19 | next(); 20 | }; 21 | 22 | 23 | 24 | var Twitter = new TwitterStream(keys); 25 | 26 | Twitter.stream('statuses/sample'); 27 | 28 | Twitter.on('connection success', function (uri) { 29 | console.log('connection success', uri); 30 | }); 31 | 32 | Twitter.on('connection aborted', function () { 33 | console.log('connection aborted'); 34 | }); 35 | 36 | Twitter.on('connection error network', function () { 37 | console.log('connection error network'); 38 | }); 39 | 40 | Twitter.on('connection error stall', function () { 41 | console.log('connection error stall'); 42 | }); 43 | 44 | Twitter.on('connection error http', function () { 45 | console.log('connection error http'); 46 | }); 47 | 48 | Twitter.on('connection rate limit', function () { 49 | console.log('connection rate limit'); 50 | }); 51 | 52 | Twitter.on('connection error unknown', function () { 53 | console.log('connection error unknown'); 54 | }); 55 | 56 | Twitter.on('data keep-alive', function () { 57 | console.log('data keep-alive'); 58 | }); 59 | 60 | Twitter.on('data error', function () { 61 | console.log('data error'); 62 | }); 63 | 64 | Twitter.pipe(Output); 65 | -------------------------------------------------------------------------------- /examples/user.basic.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | 6 | var Writable = require('stream').Writable, 7 | TwitterStream = require('../'), 8 | fs = require('fs'), 9 | keysString = fs.readFileSync('./keys.json'), 10 | keys = JSON.parse(keysString); 11 | 12 | 13 | 14 | // Helper for outputting stream to console 15 | 16 | var Output = Writable({objectMode: true}); 17 | Output._write = function (obj, enc, next) { 18 | console.log(obj.id, obj.text); 19 | next(); 20 | }; 21 | 22 | 23 | 24 | var Twitter = new TwitterStream(keys); 25 | 26 | Twitter.stream('user', { 27 | track: ['javascript'] 28 | }); 29 | 30 | Twitter.on('connection success', function (uri) { 31 | console.log('connection success', uri); 32 | }); 33 | 34 | Twitter.on('connection aborted', function () { 35 | console.log('connection aborted'); 36 | }); 37 | 38 | Twitter.on('connection error network', function () { 39 | console.log('connection error network'); 40 | }); 41 | 42 | Twitter.on('connection error stall', function () { 43 | console.log('connection error stall'); 44 | }); 45 | 46 | Twitter.on('connection error http', function () { 47 | console.log('connection error http'); 48 | }); 49 | 50 | Twitter.on('connection rate limit', function () { 51 | console.log('connection rate limit'); 52 | }); 53 | 54 | Twitter.on('connection error unknown', function () { 55 | console.log('connection error unknown'); 56 | }); 57 | 58 | Twitter.on('data keep-alive', function () { 59 | console.log('data keep-alive'); 60 | }); 61 | 62 | Twitter.on('data error', function () { 63 | console.log('data error'); 64 | }); 65 | 66 | Twitter.pipe(Output); 67 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | var pckage = require('../package.json'), 6 | querystring = require('querystring'), 7 | EventEmitter = require('events').EventEmitter, 8 | util = require('util'), 9 | request = require('request'), 10 | utils = require('core-util-is'), 11 | Parser = require('./parser.js'), 12 | 13 | endpoints = { 14 | 'statuses/sample' : {uri : 'https://stream.twitter.com/1.1/statuses/sample.json', method : 'get'}, 15 | 'statuses/filter' : {uri : 'https://stream.twitter.com/1.1/statuses/filter.json', method : 'post'}, 16 | 'statuses/firehose' : {uri : 'https://stream.twitter.com/1.1/statuses/firehose.json', method : 'get'}, 17 | 'user' : {uri : 'https://userstream.twitter.com/1.1/user.json', method : 'get'}, 18 | 'site' : {uri : 'https://sitestream.twitter.com/1.1/site.json', method : 'get'} 19 | }, 20 | newline = '\r\n', 21 | abortCodes = [401, 403, 404, 406, 412, 413, 416, 503]; 22 | 23 | 24 | 25 | var Connection = module.exports = function (keys, options) { 26 | var self = this; 27 | 28 | this.keys = keys; 29 | this.options = options; 30 | this.request = undefined; 31 | 32 | 33 | // Set up parser 34 | 35 | this.parser = new Parser(); 36 | 37 | this.parser.on('message', function (message) { 38 | if (utils.isObject(message)) { 39 | self.emit('data', message); 40 | } 41 | }); 42 | 43 | this.parser.on('error', function (error) { 44 | self.emit('data error', error); 45 | }); 46 | }; 47 | util.inherits(Connection, EventEmitter); 48 | 49 | 50 | 51 | Connection.prototype.connect = function (endpoint, params, callback) { 52 | 53 | var uri = endpoints[endpoint].uri + '?' + querystring.stringify(params), 54 | self = this, 55 | conf = { 56 | headers: { 57 | 'User-Agent': pckage.name + '/' + pckage.version + ', node.js', 58 | 'content-length': '0' 59 | }, 60 | url: uri, 61 | encoding : 'utf8', 62 | oauth: self.keys 63 | }; 64 | 65 | 66 | // Merge constructor options into request config Object 67 | 68 | if (!utils.isNullOrUndefined(self.options)) { 69 | Object.keys(self.options).forEach(function (key) { 70 | if (key === 'pool' || key === 'gzip') { 71 | conf[key] = self.options[key]; 72 | } 73 | }); 74 | } 75 | 76 | 77 | // Input parameters is invalid 78 | 79 | if (utils.isNullOrUndefined(params)) { 80 | return self.emit('connection error unknown', new Error('Illegal stream parameters provided')); 81 | } 82 | 83 | 84 | // Do request against Twitter 85 | 86 | if (endpoints[endpoint].method === 'post') { 87 | self.request = request.post(conf); 88 | } else { 89 | self.request = request.get(conf); 90 | } 91 | 92 | 93 | // Connection has stalled if data has not received after 90 seconds 94 | /* 95 | self.request.setTimeout(90000, function () { 96 | self.emit('connection error stall'); 97 | self.destroy(); 98 | }); 99 | */ 100 | 101 | self.request.on('response', function (response) { 102 | 103 | // Connection is being rate limited by Twitter 104 | 105 | if (response.statusCode === 420) { 106 | return self.emit('connection rate limit', response.statusCode); 107 | } 108 | 109 | // Connection adhere to one of Twitters defined error status codes 110 | 111 | if (abortCodes.indexOf(response.statusCode) !== -1) { 112 | return self.emit('connection error http', response.statusCode); 113 | } 114 | 115 | // Connection have an unknown error 116 | 117 | if (response.statusCode !== 200) { 118 | return self.emit('connection error unknown', response.statusCode); 119 | } 120 | 121 | 122 | self.emit('connection success', uri); 123 | if (callback) { 124 | callback.call(); 125 | } 126 | 127 | response.on('data', function (chunk) { 128 | if (chunk == newline) { 129 | return self.emit('data keep-alive'); 130 | } 131 | self.parser.parse(chunk); 132 | }); 133 | 134 | 135 | // Connection failed due to a network error 136 | 137 | response.on('error', function (error) { 138 | self.emit('connection error unknown', error); 139 | }); 140 | 141 | 142 | // Connection was closed 143 | 144 | response.on('close', function () { 145 | self.emit('connection response close'); 146 | self.request.abort(); 147 | }); 148 | 149 | }); 150 | 151 | self.request.on('error', function (error) { 152 | self.emit('connection error network', error); 153 | }); 154 | 155 | self.request.on('close', function () { 156 | // self.emit('connection error network', error); 157 | // console.log('request close'); 158 | }); 159 | 160 | self.request.end(); 161 | }; 162 | 163 | 164 | 165 | Connection.prototype.destroy = function () { 166 | this.emit('connection aborted'); 167 | this.request.abort(); 168 | }; 169 | 170 | 171 | 172 | Connection.prototype.debug = function () { 173 | return request; 174 | }; 175 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | var util = require('core-util-is'); 6 | 7 | 8 | module.exports.onBackoff = function (number, delay) { 9 | this.emit('reconnect start', number, delay); 10 | }; 11 | 12 | 13 | 14 | module.exports.onBackoffFail = function () { 15 | this.emit('reconnect aborted'); 16 | }; 17 | 18 | 19 | 20 | module.exports.parseParams = function (params) { 21 | var parsedParams = {}; 22 | 23 | if (!util.isObject(params)) { 24 | return null; 25 | } 26 | 27 | Object.keys(params).forEach(function (key) { 28 | if (util.isNullOrUndefined(params[key])) { 29 | return; 30 | } 31 | 32 | if (key === 'follow' || key === 'track' || key === 'locations') { 33 | if (util.isArray(params[key])) { 34 | if (params[key].length !== 0) { 35 | parsedParams[key] = params[key].join(','); 36 | } 37 | } 38 | 39 | if (util.isString(params[key])) { 40 | if (params[key] !== '') { 41 | parsedParams[key] = params[key]; 42 | } 43 | } 44 | 45 | return; 46 | } 47 | 48 | parsedParams[key] = params[key]; 49 | }); 50 | 51 | return (Object.keys(parsedParams).length !== 0) ? parsedParams : null; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | /** 4 | * @module twitter-stream-api 5 | */ 6 | 7 | "use strict"; 8 | 9 | var util = require('util'), 10 | emits = require('emits'), 11 | utils = require('core-util-is'), 12 | Readable = require('readable-stream').Readable, 13 | Backoff = require('backoff/lib/backoff'), 14 | ExpStrategy = require('backoff/lib/strategy/exponential'), 15 | LinStrategy = require('backoff-linear-strategy'), 16 | Connection = require('./connection.js'), 17 | helpers = require('./helpers.js'); 18 | 19 | 20 | 21 | /** 22 | * Create a new Twitter Stream API client Object 23 | * @class 24 | * 25 | * @param {Object} keys Object containing Twitter API Keys and access tokens 26 | * @param {String} keys.consumer_key Twitter consumer key (API Key) 27 | * @param {String} keys.consumer_secret Twitter consumer secret (API Secret) 28 | * @param {String} keys.token Twitter access token 29 | * @param {String} keys.token_secret Twitter access token secret 30 | * @param {(Boolean[]|Object[])} param1 If the stream should run in object mode or not. Default true. 31 | * @param {Object[]} param2 A configurations Object 32 | */ 33 | 34 | var Twitter = module.exports = function (keys, param1, param2) { 35 | var self = this; 36 | var objectMode = true; 37 | var options = {}; 38 | 39 | if (utils.isBoolean(param1)) { 40 | objectMode = param1; 41 | } 42 | 43 | if (utils.isObject(param1)) { 44 | options = param1; 45 | } 46 | 47 | if (utils.isObject(param2)) { 48 | options = param2; 49 | } 50 | 51 | this.connection = new Connection(keys, options); 52 | this.streamEndpoint = 'statuses/filter'; 53 | this.streamParams = null; 54 | 55 | Readable.call(this, {objectMode: objectMode}); 56 | this.connection.on('data', function (obj) { 57 | if (!self.push(objectMode ? obj : JSON.stringify(obj))) { 58 | self.connection.destroy(); 59 | } 60 | }); 61 | 62 | 63 | 64 | // Reconnect strategy - TCP/IP level network errors 65 | // 250 ms linearly up to 16 seconds 66 | 67 | var backoffNetworkError = new Backoff(new LinStrategy({ 68 | initialDelay: 250, 69 | maxDelay: 16000 70 | })) 71 | .on('backoff', helpers.onBackoff.bind(this)) 72 | .on('fail', helpers.onBackoffFail.bind(this)) 73 | .on('ready', function (number, delay) { 74 | self.connection.connect(self.streamEndpoint, self.streamParams, function () { 75 | backoffNetworkError.reset(); 76 | }); 77 | }); 78 | 79 | 80 | 81 | // Reconnect strategy - HTTP errors 82 | // 5 seconds exponentially up to 320 seconds 83 | 84 | var backoffHttpError = new Backoff(new ExpStrategy({ 85 | initialDelay: 5000, 86 | maxDelay: 320000 87 | })) 88 | .on('backoff', helpers.onBackoff.bind(this)) 89 | .on('fail', helpers.onBackoffFail.bind(this)) 90 | .on('ready', function (number, delay) { 91 | self.connection.connect(self.streamEndpoint, self.streamParams, function () { 92 | backoffHttpError.reset(); 93 | }); 94 | }); 95 | 96 | 97 | 98 | // Reconnect strategy - HTTP 420 errors 99 | // 1 minute exponentially for each attempt 100 | 101 | var backoffRateLimited = new Backoff(new ExpStrategy({ 102 | initialDelay: 60000, 103 | maxDelay: 1.8e+6 104 | })) 105 | .on('backoff', helpers.onBackoff.bind(this)) 106 | .on('fail', helpers.onBackoffFail.bind(this)) 107 | .on('ready', function (number, delay) { 108 | self.connection.connect(self.streamEndpoint, self.streamParams, function () { 109 | backoffRateLimited.reset(); 110 | }); 111 | }); 112 | 113 | 114 | 115 | // Proxy underlaying events 116 | 117 | this.connection.on('connection success', this.emits('connection success')); 118 | this.connection.on('connection aborted', this.emits('connection aborted')); 119 | this.connection.on('connection error network', this.emits('connection error network')); 120 | this.connection.on('connection error http', this.emits('connection error http')); 121 | this.connection.on('connection error stall', this.emits('connection error stall')); 122 | this.connection.on('connection rate limit', this.emits('connection rate limit')); 123 | this.connection.on('data keep-alive', this.emits('data keep-alive')); 124 | this.connection.on('data error', this.emits('data error')); 125 | 126 | 127 | 128 | // Handle connection errors by reconnectiong with 129 | // the suitable backoff strategy 130 | 131 | this.connection.on('connection error network', function (error) { 132 | if (backoffNetworkError.timeoutID_ === -1) { 133 | backoffNetworkError.backoff(); 134 | } 135 | }); 136 | 137 | this.connection.on('connection error stall', function (msg) { 138 | if (backoffNetworkError.timeoutID_ === -1) { 139 | backoffNetworkError.backoff(); 140 | } 141 | }); 142 | 143 | this.connection.on('connection error http', function (statusCode) { 144 | backoffHttpError.backoff(); 145 | }); 146 | 147 | this.connection.on('connection rate limit', function (statusCode) { 148 | backoffRateLimited.backoff(); 149 | }); 150 | 151 | 152 | 153 | // Unknown connection error. Terminate the stream 154 | 155 | this.connection.on('connection error unknown', function (msg) { 156 | self.emit('connection error', msg); 157 | self.close(); 158 | self.push(null); 159 | }); 160 | 161 | }; 162 | util.inherits(Twitter, Readable); 163 | 164 | 165 | 166 | Twitter.prototype.emits = emits; 167 | 168 | 169 | 170 | Twitter.prototype._read = function (size) { 171 | // Something here? 172 | }; 173 | 174 | 175 | 176 | /** 177 | * Open a stream on a given endpoint 178 | * 179 | * @param {String} endpoint The endpoint to connect to 180 | * @param {Object} parameters Object containing parameters to the endpoint 181 | */ 182 | 183 | Twitter.prototype.stream = function (endpoint, parameters) { 184 | this.streamEndpoint = endpoint; 185 | this.streamParams = helpers.parseParams(parameters); 186 | this.connection.connect(this.streamEndpoint, this.streamParams); 187 | }; 188 | 189 | 190 | 191 | /** 192 | * Close a open stream 193 | */ 194 | 195 | Twitter.prototype.close = function () { 196 | this.streamEndpoint = 'statuses/filter'; 197 | this.streamParams = null; 198 | this.connection.destroy(); 199 | }; 200 | 201 | 202 | 203 | /** 204 | * Debug method for attaching a debugger to the underlaying connection 205 | * 206 | * @param {function} callback Callback where the first argument is the underlaying connection to Twitter 207 | */ 208 | 209 | Twitter.prototype.debug = function (callback) { 210 | callback.call(null, this.connection.debug()); 211 | }; 212 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | 6 | var EventEmitter = require('events').EventEmitter, 7 | util = require('util'); 8 | 9 | 10 | 11 | var Parser = module.exports = function () { 12 | this.message = ''; 13 | }; 14 | util.inherits(Parser, EventEmitter); 15 | 16 | 17 | 18 | Parser.prototype.parse = function (chunk) { 19 | 20 | this.message += chunk; 21 | chunk = this.message; 22 | 23 | var size = chunk.length, 24 | start = 0, 25 | offset = 0, 26 | curr, 27 | next; 28 | 29 | while (offset < size) { 30 | curr = chunk[offset]; 31 | next = chunk[offset + 1]; 32 | 33 | if (curr === '\r' && next === '\n') { 34 | var piece = chunk.slice(start, offset); 35 | start = offset += 2; 36 | 37 | // Empty object 38 | 39 | if (!piece.length) { 40 | continue; 41 | } 42 | 43 | var msg; 44 | try { 45 | msg = JSON.parse(piece); 46 | } catch (error) { 47 | this.emit('error', error); 48 | } finally { 49 | if (msg) { 50 | this.emit('message', msg); 51 | continue; 52 | } 53 | } 54 | } 55 | offset++; 56 | } 57 | 58 | this.message = chunk.slice(start, size); 59 | }; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-stream-api", 3 | "version": "0.5.2", 4 | "description": "A streaming Twitter Stream API client with extended exposure of the underlaying protocol events", 5 | "main": "lib/main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:trygve-lie/twitter-stream-api.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/trygve-lie/twitter-stream-api/issues" 12 | }, 13 | "scripts": { 14 | "pretest": "jshint ./lib/*.js ./test/*.js", 15 | "test": "tap test/*.js" 16 | }, 17 | "keywords": [ 18 | "twitter", 19 | "stream" 20 | ], 21 | "author": "Trygve Lie ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "readable-stream": "^2.3.3", 25 | "core-util-is": "^1.0.2", 26 | "request": "^2.83.0", 27 | "backoff-linear-strategy": "^1.0.0", 28 | "backoff": "^2.5.0", 29 | "emits": "^3.0.0" 30 | }, 31 | "devDependencies": { 32 | "request-debug": "^0.2.0", 33 | "jshint": "^2.9.5", 34 | "nock": "^9.0.22", 35 | "tap": "^10.7.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | var tap = require('tap'), 6 | nock = require('nock'), 7 | Connection = require('../lib/connection.js'); 8 | 9 | 10 | var keys = { 11 | consumer_key: "a", 12 | consumer_secret: "b", 13 | token: "c", 14 | token_secret: "d" 15 | }; 16 | 17 | 18 | 19 | tap.test('Connection() - constructor has "pool" attribute - "maxSockets" is set in internal request.js Object', function (t) { 20 | var connection = new Connection(keys, {pool: {maxSockets: 100}}); 21 | var mockResponse = nock('https://sitestream.twitter.com') 22 | .get('/1.1/site.json?follow=1&follow=2') 23 | .reply(200, 'ok'); 24 | 25 | connection.connect('site', { 26 | follow: ['1', '2'] 27 | }, function () { 28 | t.equal(connection.request.pool.maxSockets, 100); 29 | t.end(); 30 | }); 31 | }); 32 | 33 | 34 | 35 | tap.test('Connection() - constructor has no "pool" attribute - "maxSockets" is "undefined" ', function (t) { 36 | var connection = new Connection(keys); 37 | var mockResponse = nock('https://sitestream.twitter.com') 38 | .get('/1.1/site.json?follow=1&follow=2') 39 | .reply(200, 'ok'); 40 | 41 | connection.connect('site', { 42 | follow: ['1', '2'] 43 | }, function () { 44 | t.equal(connection.request.pool.maxSockets, undefined); 45 | t.end(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | var tap = require('tap'), 6 | helpers = require('../lib/helpers.js'); 7 | 8 | 9 | 10 | tap.test('helpers.parseParams() - parameters is not an Object - shall return null', function (t) { 11 | var conf = "gibberish"; 12 | var result = helpers.parseParams(conf); 13 | t.type(result, 'null'); 14 | t.end(); 15 | }); 16 | 17 | 18 | 19 | tap.test('helpers.parseParams() - parameters is an empty Object - shall return null', function (t) { 20 | var conf = {}; 21 | var result = helpers.parseParams(conf); 22 | t.type(result, 'null'); 23 | t.end(); 24 | }); 25 | 26 | 27 | 28 | tap.test('helpers.parseParams() - parameters is not an empty Object - shall not return the same Object', function (t) { 29 | var conf = { 30 | stall_warnings : true, 31 | follow : ['123'], 32 | track : ['abc'], 33 | locations : ['-74,40,-73,41'] 34 | }; 35 | var result = helpers.parseParams(conf); 36 | t.type(result, 'object'); 37 | t.notSame(result, conf); 38 | t.end(); 39 | }); 40 | 41 | 42 | 43 | tap.test('helpers.parseParams() - "track", "follow" and "locations" has Arrays as values - shall return copied Object where these values are Strings', function (t) { 44 | var conf = { 45 | stall_warnings : true, 46 | follow : ['123'], 47 | track : ['abc'], 48 | locations : ['-74,40,-73,41'] 49 | }; 50 | var result = helpers.parseParams(conf); 51 | t.type(result, 'object'); 52 | t.type(result.follow, 'string'); 53 | t.type(result.track, 'string'); 54 | t.type(result.locations, 'string'); 55 | t.end(); 56 | }); 57 | 58 | 59 | 60 | tap.test('helpers.parseParams() - "track", "follow" and "locations" has Strings as values - shall return copied Object where these values are Strings', function (t) { 61 | var conf = { 62 | stall_warnings : true, 63 | follow : '123', 64 | track : 'abc', 65 | locations : '-74,40,-73,41' 66 | }; 67 | var result = helpers.parseParams(conf); 68 | t.type(result, 'object'); 69 | t.type(result.follow, 'string'); 70 | t.type(result.track, 'string'); 71 | t.type(result.locations, 'string'); 72 | t.end(); 73 | }); 74 | 75 | 76 | 77 | tap.test('helpers.parseParams() - "stall_warnings" has Boolean value - shall return copied Object where "stall_warnings" has Boolean value', function (t) { 78 | var conf = { 79 | stall_warnings : true, 80 | follow : ['123'], 81 | track : ['abc'], 82 | locations : ['-74,40,-73,41'] 83 | }; 84 | var result = helpers.parseParams(conf); 85 | t.type(result.stall_warnings, 'boolean'); 86 | t.end(); 87 | }); 88 | 89 | 90 | tap.test('helpers.parseParams() - "follow" is not set - shall return copied Object without "follow"', function (t) { 91 | var conf = { 92 | track : ['abc'], 93 | locations : ['-74,40,-73,41'] 94 | }; 95 | var result = helpers.parseParams(conf); 96 | t.type(result.follow, 'undefined'); 97 | t.type(result.track, 'string'); 98 | t.type(result.locations, 'string'); 99 | t.end(); 100 | }); 101 | 102 | 103 | 104 | tap.test('helpers.parseParams() - "track" is not set - shall return copied Object without "track"', function (t) { 105 | var conf = { 106 | follow : ['123'], 107 | locations : ['-74,40,-73,41'] 108 | }; 109 | var result = helpers.parseParams(conf); 110 | t.type(result.follow, 'string'); 111 | t.type(result.track, 'undefined'); 112 | t.type(result.locations, 'string'); 113 | t.end(); 114 | }); 115 | 116 | 117 | 118 | tap.test('helpers.parseParams() - "locations" is not set - shall return copied Object without "locations"', function (t) { 119 | var conf = { 120 | follow : ['123'], 121 | track : ['abc'] 122 | }; 123 | var result = helpers.parseParams(conf); 124 | t.type(result.follow, 'string'); 125 | t.type(result.track, 'string'); 126 | t.type(result.locations, 'undefined'); 127 | t.end(); 128 | }); 129 | 130 | 131 | 132 | tap.test('helpers.parseParams() - "follow" is an empty Array - shall return copied Object without "follow"', function (t) { 133 | var conf = { 134 | follow : [], 135 | track : ['abc'], 136 | locations : ['-74,40,-73,41'] 137 | }; 138 | var result = helpers.parseParams(conf); 139 | t.type(result.follow, 'undefined'); 140 | t.type(result.track, 'string'); 141 | t.type(result.locations, 'string'); 142 | t.end(); 143 | }); 144 | 145 | 146 | 147 | tap.test('helpers.parseParams() - "track" is an empty Array - shall return copied Object without "track"', function (t) { 148 | var conf = { 149 | follow : ['123'], 150 | track : [], 151 | locations : ['-74,40,-73,41'] 152 | }; 153 | var result = helpers.parseParams(conf); 154 | t.type(result.follow, 'string'); 155 | t.type(result.track, 'undefined'); 156 | t.type(result.locations, 'string'); 157 | t.end(); 158 | }); 159 | 160 | 161 | 162 | tap.test('helpers.parseParams() - "locations" is an empty Array - shall return copied Object without "locations"', function (t) { 163 | var conf = { 164 | follow : ['123'], 165 | track : ['abc'], 166 | locations : [] 167 | }; 168 | var result = helpers.parseParams(conf); 169 | t.type(result.follow, 'string'); 170 | t.type(result.track, 'string'); 171 | t.type(result.locations, 'undefined'); 172 | t.end(); 173 | }); 174 | 175 | 176 | 177 | tap.test('helpers.parseParams() - "follow", "track" and "locations" have multiple entries in the Array - values shall be joined with a ","', function (t) { 178 | var conf = { 179 | follow : ['123','456','789'], 180 | track : ['abc','def','ghi'], 181 | locations : ['-74,40,-73,41','-74,60,-73,61','-74,80,-73,81'] 182 | }; 183 | var result = helpers.parseParams(conf); 184 | t.equal(result.follow, '123,456,789'); 185 | t.equal(result.track, 'abc,def,ghi'); 186 | t.equal(result.locations, '-74,40,-73,41,-74,60,-73,61,-74,80,-73,81'); 187 | t.end(); 188 | }); 189 | 190 | 191 | 192 | tap.test('helpers.parseParams() - "follow" is an empty String - shall return copied Object without "follow"', function (t) { 193 | var conf = { 194 | follow : '', 195 | track : ['abc'], 196 | locations : ['-74,40,-73,41'] 197 | }; 198 | var result = helpers.parseParams(conf); 199 | t.type(result.follow, 'undefined'); 200 | t.type(result.track, 'string'); 201 | t.type(result.locations, 'string'); 202 | t.end(); 203 | }); 204 | 205 | 206 | 207 | tap.test('helpers.parseParams() - "track" is an empty Array - shall return copied Object without "track"', function (t) { 208 | var conf = { 209 | follow : ['123'], 210 | track : '', 211 | locations : ['-74,40,-73,41'] 212 | }; 213 | var result = helpers.parseParams(conf); 214 | t.type(result.follow, 'string'); 215 | t.type(result.track, 'undefined'); 216 | t.type(result.locations, 'string'); 217 | t.end(); 218 | }); 219 | 220 | 221 | 222 | tap.test('helpers.parseParams() - "locations" is an empty Array - shall return copied Object without "locations"', function (t) { 223 | var conf = { 224 | follow : ['123'], 225 | track : ['abc'], 226 | locations : '' 227 | }; 228 | var result = helpers.parseParams(conf); 229 | t.type(result.follow, 'string'); 230 | t.type(result.track, 'string'); 231 | t.type(result.locations, 'undefined'); 232 | t.end(); 233 | }); 234 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, strict: true */ 2 | 3 | "use strict"; 4 | 5 | var tap = require('tap'), 6 | TwitterStream = require('../lib/main.js'); 7 | 8 | 9 | var keys = { 10 | consumer_key: "a", 11 | consumer_secret: "b", 12 | token: "c", 13 | token_secret: "d" 14 | }; 15 | 16 | 17 | 18 | tap.test('Main() - no rest attributes set - "objectMode" should be "true" - "options" should be empty Object', function (t) { 19 | var Twitter = new TwitterStream(keys); 20 | t.equal(Twitter._readableState.objectMode, true); 21 | t.same(Twitter.connection.options, {}); 22 | t.end(); 23 | }); 24 | 25 | 26 | tap.test('Main() - 1st rest attribute is "true" - "objectMode" should be "true" - "options" should be empty Object', function (t) { 27 | var Twitter = new TwitterStream(keys, true); 28 | t.equal(Twitter._readableState.objectMode, true); 29 | t.same(Twitter.connection.options, {}); 30 | t.end(); 31 | }); 32 | 33 | 34 | tap.test('Main() - 1st rest attribute is "false" - "objectMode" should be "false" - "options" should be empty Object', function (t) { 35 | var Twitter = new TwitterStream(keys, false); 36 | t.equal(Twitter._readableState.objectMode, false); 37 | t.same(Twitter.connection.options, {}); 38 | t.end(); 39 | }); 40 | 41 | 42 | tap.test('Main() - 1st rest attribute is a Object - "objectMode" should be "true" - "options" should be same Object', function (t) { 43 | var Twitter = new TwitterStream(keys, {gzip : true}); 44 | t.equal(Twitter._readableState.objectMode, true); 45 | t.same(Twitter.connection.options, {gzip : true}); 46 | t.end(); 47 | }); 48 | 49 | 50 | tap.test('Main() - 1st rest attribute is "true" - 2nd rest attribute is an Object - "objectMode" should be "true" - "options" should be same Object', function (t) { 51 | var Twitter = new TwitterStream(keys, true, {gzip : true}); 52 | t.equal(Twitter._readableState.objectMode, true); 53 | t.same(Twitter.connection.options, {gzip : true}); 54 | t.end(); 55 | }); 56 | 57 | 58 | tap.test('Main() - 1st rest attribute is "false" - 2nd rest attribute is an Object - "objectMode" should be "true" - "options" should be same Object', function (t) { 59 | var Twitter = new TwitterStream(keys, false, {gzip : true}); 60 | t.equal(Twitter._readableState.objectMode, false); 61 | t.same(Twitter.connection.options, {gzip : true}); 62 | t.end(); 63 | }); 64 | --------------------------------------------------------------------------------