├── .gitignore ├── .npmignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── Rakefile ├── api.js ├── browser ├── eventemitter2.js ├── export.js ├── index.html ├── jquery-1.6.1.min.js ├── log4js.js ├── main.js ├── querystring.js ├── request.jquery.js ├── require.js └── util.js ├── cli.js ├── lib ├── feed.js ├── index.js └── stream.js ├── package.json └── test ├── couch.js ├── follow.js ├── issues.js ├── issues ├── 10.js └── 43.js └── stream.js /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ./.gitignore 2 | ./Rakefile 3 | ./browser/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - curl --location http://git.io/1OcIZA | bash -s 4 | services: 5 | - couchdb 6 | node_js: 7 | - "0.8" 8 | - "0.10" 9 | - "0.12" 10 | - "iojs" 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - node_js: "0.12" 16 | - node_js: "iojs" 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jason Smith Work 2 | Jason Smith 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Follow: CouchDB changes and db updates notifier for NodeJS 2 | 3 | [![build 4 | status](https://secure.travis-ci.org/iriscouch/follow.png)](http://travis-ci.org/iriscouch/follow) 5 | 6 | Follow (upper-case *F*) comes from an internal Iris Couch project used in production for over a year. It works in the browser (beta) and is available as an NPM module. 7 | 8 | $ npm install follow 9 | 10 | ## Example 11 | 12 | This looks much like the [request][req] API. 13 | 14 | ```javascript 15 | var follow = require('follow'); 16 | follow("https://example.iriscouch.com/boogie", function(error, change) { 17 | if(!error) { 18 | console.log("Got change number " + change.seq + ": " + change.id); 19 | } 20 | }) 21 | ``` 22 | 23 | The *error* parameter to the callback will basically always be `null`. 24 | 25 | ## Objective 26 | 27 | The API must be very simple: notify me every time a change happens in the DB. Also, never fail. 28 | 29 | If an error occurs, Follow will internally retry without notifying your code. 30 | 31 | Specifically, this should be possible: 32 | 33 | 1. Begin a changes feed. Get a couple of change callbacks 34 | 2. Shut down CouchDB 35 | 3. Go home. Have a nice weekend. Come back on Monday. 36 | 4. Start CouchDB with a different IP address 37 | 5. Make a couple of changes 38 | 6. Update DNS so the domain points to the new IP 39 | 7. Once DNS propagates, get a couple more change callbacks 40 | 41 | ## Failure Mode 42 | 43 | If CouchDB permanently crashes, there is an option of failure modes: 44 | 45 | * **Default:** Simply never call back with a change again 46 | * **Optional:** Specify an *inactivity* timeout. If no changes happen by the timeout, Follow will signal an error. 47 | 48 | ## DB Updates 49 | 50 | If the db url ends with `/_db_updates`, Follow will provide a 51 | [_db_updates](http://docs.couchdb.org/en/latest/api/server/common.html?highlight=db_updates#get--_db_updates) feed. 52 | 53 | For each change, Follow will emit a `change` event containing: 54 | 55 | * `type`: `created`, `updated` or `deleted`. 56 | * `db_name`: Name of the database where the change occoured. 57 | * `ok`: Event operation status (boolean). 58 | 59 | Note that this feature is available as of CouchDB 1.4. 60 | 61 | ### Simple API: follow(options, callback) 62 | 63 | The first argument is an options object. The only required option is `db`. Instead of an object, you can use a string to indicate the `db` value. 64 | 65 | ```javascript 66 | follow({db:"https://example.iriscouch.com/boogie", include_docs:true}, function(error, change) { 67 | if(!error) { 68 | console.log("Change " + change.seq + " has " + Object.keys(change.doc).length + " fields"); 69 | } 70 | }) 71 | ``` 72 | 73 | 74 | All of the CouchDB _changes options are allowed. See http://guide.couchdb.org/draft/notifications.html. 75 | 76 | * `db` | Fully-qualified URL of a couch database. (Basic auth URLs are ok.) 77 | * `since` | The sequence number to start from. Use `"now"` to start from the latest change in the DB. 78 | * `heartbeat` | Milliseconds within which CouchDB must respond (default: **30000** or 30 seconds) 79 | * `feed` | **Optional but only "continuous" is allowed** 80 | * `filter` | 81 | * **Either** a path to design document filter, e.g. `app/important` 82 | * **Or** a Javascript `function(doc, req) { ... }` which should return true or false 83 | * `query_params` | **Optional** for use in with `filter` functions, passed as `req.query` to the filter function 84 | 85 | Besides the CouchDB options, more are available: 86 | 87 | * `headers` | Object with HTTP headers to add to the request 88 | * `inactivity_ms` | Maximum time to wait between **changes**. Omitting this means no maximum. 89 | * `max_retry_seconds` | Maximum time to wait between retries (default: 360 seconds) 90 | * `initial_retry_delay` | Time to wait before the first retry, in milliseconds (default 1000 milliseconds) 91 | * `response_grace_time` | Extra time to wait before timing out, in milliseconds (default 5000 milliseconds) 92 | 93 | ## Object API 94 | 95 | The main API is a thin wrapper around the EventEmitter API. 96 | 97 | ```javascript 98 | var follow = require('follow'); 99 | 100 | var opts = {}; // Same options paramters as before 101 | var feed = new follow.Feed(opts); 102 | 103 | // You can also set values directly. 104 | feed.db = "http://example.iriscouch.com/boogie"; 105 | feed.since = 3; 106 | feed.heartbeat = 30 * 1000 107 | feed.inactivity_ms = 86400 * 1000; 108 | 109 | feed.filter = function(doc, req) { 110 | // req.query is the parameters from the _changes request and also feed.query_params. 111 | console.log('Filtering for query: ' + JSON.stringify(req.query)); 112 | 113 | if(doc.stinky || doc.ugly) 114 | return false; 115 | return true; 116 | } 117 | 118 | feed.on('change', function(change) { 119 | console.log('Doc ' + change.id + ' in change ' + change.seq + ' is neither stinky nor ugly.'); 120 | }) 121 | 122 | feed.on('error', function(er) { 123 | console.error('Since Follow always retries on errors, this must be serious'); 124 | throw er; 125 | }) 126 | 127 | feed.follow(); 128 | ``` 129 | 130 | 131 | ## Pause and Resume 132 | 133 | A Follow feed is a Node.js stream. If you get lots of changes and processing them takes a while, use `.pause()` and `.resume()` as needed. Pausing guarantees that no new events will fire. Resuming guarantees you'll pick up where you left off. 134 | 135 | ```javascript 136 | follow("https://example.iriscouch.com/boogie", function(error, change) { 137 | var feed = this 138 | 139 | if(change.seq == 1) { 140 | console.log('Uh oh. The first change takes 30 hours to process. Better pause.') 141 | feed.pause() 142 | setTimeout(function() { feed.resume() }, 30 * 60 * 60 * 1000) 143 | } 144 | 145 | // ... 30 hours with no events ... 146 | 147 | else 148 | console.log('No need to pause for normal change: ' + change.id) 149 | }) 150 | ``` 151 | 152 | 153 | ## Events 154 | 155 | The feed object is an EventEmitter. There are a few ways to get a feed object: 156 | 157 | * Use the object API above 158 | * Use the return value of `follow()` 159 | * In the callback to `follow()`, the *this* variable is bound to the feed object. 160 | 161 | Once you've got one, you can subscribe to these events: 162 | 163 | * **start** | Before any i/o occurs 164 | * **confirm_request** | `function(req)` | The database confirmation request is sent; passed the `request` object 165 | * **confirm** | `function(db_obj)` | The database is confirmed; passed the couch database object 166 | * **change** | `function(change)` | A change occured; passed the change object from CouchDB 167 | * **catchup** | `function(seq_id)` | The feed has caught up to the *update_seq* from the confirm step. Assuming no subsequent changes, you have seen all the data. 168 | * **wait** | Follow is idle, waiting for the next data chunk from CouchDB 169 | * **timeout** | `function(info)` | Follow did not receive a heartbeat from couch in time. The passed object has `.elapsed_ms` set to the elapsed time 170 | * **retry** | `function(info)` | A retry is scheduled (usually after a timeout or disconnection). The passed object has 171 | * `.since` the current sequence id 172 | * `.after` the milliseconds to wait before the request occurs (on an exponential fallback schedule) 173 | * `.db` the database url (scrubbed of basic auth credentials) 174 | * **stop** | The feed is stopping, because of an error, or because you called `feed.stop()` 175 | * **error** | `function(err)` | An error occurs 176 | 177 | ## Error conditions 178 | 179 | Follow is happy to retry over and over, for all eternity. It will only emit an error if it thinks your whole application might be in trouble. 180 | 181 | * *DB confirmation* failed: Follow confirms the DB with a preliminary query, which must reply properly. 182 | * *DB is deleted*: Even if it retried, subsequent sequence numbers would be meaningless to your code. 183 | * *Your inactivity timer* expired: This is a last-ditch way to detect possible errors. What if couch is sending heartbeats just fine, but nothing has changed for 24 hours? You know that for your app, 24 hours with no change is impossible. Maybe your filter has a bug? Maybe you queried the wrong DB? Whatever the reason, Follow will emit an error. 184 | * JSON parse error, which should be impossible from CouchDB 185 | * Invalid change object format, which should be impossible from CouchDB 186 | * Internal error, if the internal state seems wrong, e.g. cancelling a timeout that already expired, etc. Follow tries to fail early. 187 | 188 | ## Tests 189 | 190 | Follow uses [node-tap][tap]. If you clone this Git repository, tap is included. 191 | 192 | $ ./node_modules/.bin/tap test/*.js test/issues/*.js 193 | ok test/couch.js ...................................... 11/11 194 | ok test/follow.js ..................................... 69/69 195 | ok test/issues.js ..................................... 44/44 196 | ok test/stream.js ................................... 300/300 197 | ok test/issues/10.js .................................. 11/11 198 | total ............................................... 435/435 199 | 200 | ok 201 | 202 | ## License 203 | 204 | Apache 2.0 205 | 206 | [req]: https://github.com/mikeal/request 207 | [tap]: https://github.com/isaacs/node-tap 208 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | LIBRARY = [ "api.js", "feed.js", "lib.js", "cli.js" ] 4 | BROWSER_TOOLS = Dir.glob("browser/*").select{|x| x != "browser/export.js" } 5 | 6 | task :default => :export 7 | 8 | desc 'Export Follow for use in web browsers' 9 | task :export => ([:clean, "cli.js"] + LIBRARY + BROWSER_TOOLS) do |task| 10 | build = "./build" 11 | target = "#{build}/follow" 12 | 13 | sh "mkdir", "-p", target 14 | LIBRARY.each do |js| 15 | sh "node", "browser/export.js", js, "#{target}/#{js}" 16 | end 17 | 18 | BROWSER_TOOLS.each do |file| 19 | sh "cp", file, build 20 | end 21 | 22 | # EventEmitter2 needs wrapping. 23 | sh "node", "browser/export.js", "browser/eventemitter2.js", "#{build}/eventemitter2.js" 24 | 25 | File.open("#{build}/boot.js", 'w') do |boot| 26 | requirejs_paths = { 'request' => 'request.jquery', 27 | 'events' => 'eventemitter2', 28 | # 'foo' => 'bar', etc. 29 | } 30 | 31 | opts = { # 'baseUrl' => "follow", 32 | 'paths' => requirejs_paths, 33 | } 34 | 35 | main_js = "main.js" 36 | 37 | boot.write([ 'require(', 38 | '// Options', 39 | opts.to_json() + ',', 40 | '', 41 | '// Modules', 42 | [main_js].to_json() + ',', 43 | '', 44 | '// Code to run when ready', 45 | 'function(main) { return main(); }', 46 | ');' 47 | ].join("\n")); 48 | end 49 | end 50 | 51 | desc 'Clean up build files' 52 | task :clean do 53 | sh "rm", "-rf", "./build" 54 | end 55 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | // The changes_couchdb API 2 | // 3 | // Copyright 2011 Jason Smith, Jarrett Cruger and contributors 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | var feed = require('./lib/feed') 18 | , stream = require('./lib/stream') 19 | 20 | function follow_feed(opts, cb) { 21 | var ch_feed = new feed.Feed(opts); 22 | ch_feed.on('error' , function(er) { return cb && cb.call(ch_feed, er) }); 23 | ch_feed.on('change', function(ch) { return cb && cb.call(ch_feed, null, ch) }); 24 | 25 | // Give the caller a chance to hook into any events. 26 | process.nextTick(function() { 27 | ch_feed.follow(); 28 | }) 29 | 30 | return ch_feed; 31 | } 32 | 33 | module.exports = follow_feed; 34 | module.exports.Feed = feed.Feed; 35 | module.exports.Changes = stream.Changes 36 | -------------------------------------------------------------------------------- /browser/eventemitter2.js: -------------------------------------------------------------------------------- 1 | 2 | ;!function(exports, undefined) { 3 | 4 | var isArray = Array.isArray; 5 | var defaultMaxListeners = 10; 6 | 7 | function init() { 8 | this._events = new Object; 9 | } 10 | 11 | function configure(conf) { 12 | 13 | if (conf) { 14 | this.wildcard = conf.wildcard; 15 | this.delimiter = conf.delimiter || '.'; 16 | 17 | if (this.wildcard) { 18 | this.listenerTree = new Object; 19 | } 20 | } 21 | } 22 | 23 | function EventEmitter(conf) { 24 | this._events = new Object; 25 | configure.call(this, conf); 26 | } 27 | 28 | function searchListenerTree(handlers, type, tree, i) { 29 | if (!tree) { 30 | return; 31 | } 32 | 33 | var listeners; 34 | 35 | if (i === type.length && tree._listeners) { 36 | // 37 | // If at the end of the event(s) list and the tree has listeners 38 | // invoke those listeners. 39 | // 40 | if (typeof tree._listeners === 'function') { 41 | handlers && handlers.push(tree._listeners); 42 | return tree; 43 | } else { 44 | for (var leaf = 0, len = tree._listeners.length; leaf < len; leaf++) { 45 | handlers && handlers.push(tree._listeners[leaf]); 46 | } 47 | return tree; 48 | } 49 | } 50 | 51 | if (type[i] === '*' || tree[type[i]]) { 52 | // 53 | // If the event emitted is '*' at this part 54 | // or there is a concrete match at this patch 55 | // 56 | if (type[i] === '*') { 57 | for (var branch in tree) { 58 | if (branch !== '_listeners' && tree.hasOwnProperty(branch)) { 59 | listeners = searchListenerTree(handlers, type, tree[branch], i+1); 60 | } 61 | } 62 | return listeners; 63 | } 64 | 65 | listeners = searchListenerTree(handlers, type, tree[type[i]], i+1); 66 | } 67 | 68 | 69 | if (tree['*']) { 70 | // 71 | // If the listener tree will allow any match for this part, 72 | // then recursively explore all branches of the tree 73 | // 74 | searchListenerTree(handlers, type, tree['*'], i+1); 75 | } 76 | 77 | return listeners; 78 | }; 79 | 80 | function growListenerTree(type, listener) { 81 | 82 | type = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); 83 | 84 | var tree = this.listenerTree; 85 | var name = type.shift(); 86 | 87 | while (name) { 88 | 89 | if (!tree[name]) { 90 | tree[name] = new Object; 91 | } 92 | 93 | tree = tree[name]; 94 | 95 | if (type.length === 0) { 96 | 97 | if (!tree._listeners) { 98 | tree._listeners = listener; 99 | } 100 | else if(typeof tree._listeners === 'function') { 101 | tree._listeners = [tree._listeners, listener]; 102 | } 103 | else if (isArray(tree._listeners)) { 104 | 105 | tree._listeners.push(listener); 106 | 107 | if (!tree._listeners.warned) { 108 | 109 | var m = defaultMaxListeners; 110 | 111 | if (m > 0 && tree._listeners.length > m) { 112 | 113 | tree._listeners.warned = true; 114 | console.error('(node) warning: possible EventEmitter memory ' + 115 | 'leak detected. %d listeners added. ' + 116 | 'Use emitter.setMaxListeners() to increase limit.', 117 | tree._listeners.length); 118 | console.trace(); 119 | } 120 | } 121 | } 122 | return true; 123 | } 124 | name = type.shift(); 125 | } 126 | return true; 127 | }; 128 | 129 | // By default EventEmitters will print a warning if more than 130 | // 10 listeners are added to it. This is a useful default which 131 | // helps finding memory leaks. 132 | // 133 | // Obviously not all Emitters should be limited to 10. This function allows 134 | // that to be increased. Set to zero for unlimited. 135 | 136 | EventEmitter.prototype.setMaxListeners = function(n) { 137 | this._events || init.call(this); 138 | this._events.maxListeners = n; 139 | }; 140 | 141 | EventEmitter.prototype.event = ''; 142 | 143 | EventEmitter.prototype.once = function(event, fn) { 144 | this.many(event, 1, fn); 145 | return this; 146 | }; 147 | 148 | EventEmitter.prototype.many = function(event, ttl, fn) { 149 | var self = this; 150 | 151 | if (typeof fn !== 'function') { 152 | throw new Error('many only accepts instances of Function'); 153 | } 154 | 155 | function listener() { 156 | if (--ttl === 0) { 157 | self.off(event, listener); 158 | } 159 | fn.apply(null, arguments); 160 | }; 161 | 162 | listener._origin = fn; 163 | 164 | this.on(event, listener); 165 | 166 | return self; 167 | }; 168 | 169 | EventEmitter.prototype.emit = function() { 170 | this._events || init.call(this); 171 | 172 | var type = arguments[0]; 173 | 174 | if (type === 'newListener') { 175 | if (!this._events.newListener) { return false; } 176 | } 177 | 178 | // Loop through the *_all* functions and invoke them. 179 | if (this._all) { 180 | var l = arguments.length; 181 | var args = new Array(l - 1); 182 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 183 | for (i = 0, l = this._all.length; i < l; i++) { 184 | this.event = type; 185 | this._all[i].apply(this, args); 186 | } 187 | } 188 | 189 | // If there is no 'error' event listener then throw. 190 | if (type === 'error') { 191 | 192 | if (!this._all && 193 | !this._events.error && 194 | !(this.wildcard && this.listenerTree.error)) { 195 | 196 | if (arguments[1] instanceof Error) { 197 | throw arguments[1]; // Unhandled 'error' event 198 | } else { 199 | throw new Error("Uncaught, unspecified 'error' event."); 200 | } 201 | return false; 202 | } 203 | } 204 | 205 | var handler; 206 | 207 | if(this.wildcard) { 208 | handler = []; 209 | var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); 210 | searchListenerTree.call(this, handler, ns, this.listenerTree, 0); 211 | } 212 | else { 213 | handler = this._events[type]; 214 | } 215 | 216 | if (typeof handler === 'function') { 217 | this.event = type; 218 | if (arguments.length === 1) { 219 | handler.call(this); 220 | } 221 | else if (arguments.length > 1) 222 | switch (arguments.length) { 223 | case 2: 224 | handler.call(this, arguments[1]); 225 | break; 226 | case 3: 227 | handler.call(this, arguments[1], arguments[2]); 228 | break; 229 | // slower 230 | default: 231 | var l = arguments.length; 232 | var args = new Array(l - 1); 233 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 234 | handler.apply(this, args); 235 | } 236 | return true; 237 | } 238 | else if (handler) { 239 | var l = arguments.length; 240 | var args = new Array(l - 1); 241 | for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; 242 | 243 | var listeners = handler.slice(); 244 | for (var i = 0, l = listeners.length; i < l; i++) { 245 | this.event = type; 246 | listeners[i].apply(this, args); 247 | } 248 | return true; 249 | } 250 | 251 | }; 252 | 253 | EventEmitter.prototype.on = function(type, listener) { 254 | this._events || init.call(this); 255 | 256 | // To avoid recursion in the case that type == "newListeners"! Before 257 | // adding it to the listeners, first emit "newListeners". 258 | this.emit('newListener', type, listener); 259 | 260 | if(this.wildcard) { 261 | growListenerTree.call(this, type, listener); 262 | return this; 263 | } 264 | 265 | if (!this._events[type]) { 266 | // Optimize the case of one listener. Don't need the extra array object. 267 | this._events[type] = listener; 268 | } 269 | else if(typeof this._events[type] === 'function') { 270 | // Adding the second element, need to change to array. 271 | this._events[type] = [this._events[type], listener]; 272 | } 273 | else if (isArray(this._events[type])) { 274 | // If we've already got an array, just append. 275 | this._events[type].push(listener); 276 | 277 | // Check for listener leak 278 | if (!this._events[type].warned) { 279 | 280 | var m; 281 | if (this._events.maxListeners !== undefined) { 282 | m = this._events.maxListeners; 283 | } else { 284 | m = defaultMaxListeners; 285 | } 286 | 287 | if (m && m > 0 && this._events[type].length > m) { 288 | 289 | this._events[type].warned = true; 290 | console.error('(node) warning: possible EventEmitter memory ' + 291 | 'leak detected. %d listeners added. ' + 292 | 'Use emitter.setMaxListeners() to increase limit.', 293 | this._events[type].length); 294 | console.trace(); 295 | } 296 | } 297 | } 298 | return this; 299 | }; 300 | 301 | EventEmitter.prototype.onAny = function(fn) { 302 | 303 | if(!this._all) { 304 | this._all = []; 305 | } 306 | 307 | if (typeof fn !== 'function') { 308 | throw new Error('onAny only accepts instances of Function'); 309 | } 310 | 311 | // Add the function to the event listener collection. 312 | this._all.push(fn); 313 | return this; 314 | }; 315 | 316 | EventEmitter.prototype.addListener = EventEmitter.prototype.on; 317 | 318 | EventEmitter.prototype.off = function(type, listener) { 319 | if (typeof listener !== 'function') { 320 | throw new Error('removeListener only takes instances of Function'); 321 | } 322 | 323 | var handlers; 324 | 325 | if(this.wildcard) { 326 | var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); 327 | var leaf = searchListenerTree.call(this, null, ns, this.listenerTree, 0); 328 | 329 | if('undefined' === typeof leaf) { return this; } 330 | handlers = leaf._listeners; 331 | } 332 | else { 333 | // does not use listeners(), so no side effect of creating _events[type] 334 | if (!this._events[type]) return this; 335 | handlers = this._events[type]; 336 | } 337 | 338 | if (isArray(handlers)) { 339 | 340 | var position = -1; 341 | 342 | for (var i = 0, length = handlers.length; i < length; i++) { 343 | if (handlers[i] === listener || 344 | (handlers[i].listener && handlers[i].listener === listener) || 345 | (handlers[i]._origin && handlers[i]._origin === listener)) { 346 | position = i; 347 | break; 348 | } 349 | } 350 | 351 | if (position < 0) { 352 | return this; 353 | } 354 | 355 | if(this.wildcard) { 356 | leaf._listeners.splice(position, 1) 357 | } 358 | else { 359 | this._events[type].splice(position, 1); 360 | } 361 | 362 | if (handlers.length === 0) { 363 | if(this.wildcard) { 364 | delete leaf._listeners; 365 | } 366 | else { 367 | delete this._events[type]; 368 | } 369 | } 370 | } 371 | else if (handlers === listener || 372 | (handlers.listener && handlers.listener === listener) || 373 | (handlers._origin && handlers._origin === listener)) { 374 | if(this.wildcard) { 375 | delete leaf._listeners; 376 | } 377 | else { 378 | delete this._events[type]; 379 | } 380 | } 381 | 382 | return this; 383 | }; 384 | 385 | EventEmitter.prototype.offAny = function(fn) { 386 | var i = 0, l = 0, fns; 387 | if (fn && this._all && this._all.length > 0) { 388 | fns = this._all; 389 | for(i = 0, l = fns.length; i < l; i++) { 390 | if(fn === fns[i]) { 391 | fns.splice(i, 1); 392 | return this; 393 | } 394 | } 395 | } else { 396 | this._all = []; 397 | } 398 | return this; 399 | }; 400 | 401 | EventEmitter.prototype.removeListener = EventEmitter.prototype.off; 402 | 403 | EventEmitter.prototype.removeAllListeners = function(type) { 404 | if (arguments.length === 0) { 405 | !this._events || init.call(this); 406 | return this; 407 | } 408 | 409 | if(this.wildcard) { 410 | var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); 411 | var leaf = searchListenerTree.call(this, null, ns, this.listenerTree, 0); 412 | 413 | if('undefined' === typeof leaf) { return this; } 414 | leaf._listeners = null; 415 | } 416 | else { 417 | if (!this._events[type]) return this; 418 | this._events[type] = null; 419 | } 420 | return this; 421 | }; 422 | 423 | EventEmitter.prototype.listeners = function(type) { 424 | if(this.wildcard) { 425 | var handlers = []; 426 | var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); 427 | searchListenerTree.call(this, handlers, ns, this.listenerTree, 0); 428 | return handlers; 429 | } 430 | 431 | this._events || init.call(this); 432 | 433 | if (!this._events[type]) this._events[type] = []; 434 | if (!isArray(this._events[type])) { 435 | this._events[type] = [this._events[type]]; 436 | } 437 | return this._events[type]; 438 | }; 439 | 440 | EventEmitter.prototype.listenersAny = function() { 441 | 442 | if(this._all) { 443 | return this._all; 444 | } 445 | else { 446 | return []; 447 | } 448 | 449 | }; 450 | 451 | exports.EventEmitter2 = EventEmitter; 452 | 453 | }(typeof exports === 'undefined' ? window : exports); 454 | -------------------------------------------------------------------------------- /browser/export.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Export Follow in browser-friendly format. 3 | // 4 | // Copyright 2011 Jason Smith, Jarrett Cruger and contributors 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | var fs = require('fs') 19 | , path = require('path') 20 | ; 21 | 22 | if(process.argv[1] === module.filename) { 23 | var source = process.argv[2] 24 | , dest = process.argv[3] 25 | ; 26 | 27 | if(!source || !dest) 28 | throw new Error("usage: browser-export.js "); 29 | 30 | install(source, dest, function(er) { 31 | if(er) throw er; 32 | }); 33 | } 34 | 35 | function install(filename, target, callback) { 36 | //console.log('Exporting: ' + filename); 37 | fs.readFile(filename, null, function(er, content) { 38 | if(er && er.errno) er = new Error(er.stack); // Make a better stack trace. 39 | if(er) return callback(er); 40 | 41 | // Strip the shebang. 42 | content = content.toString('utf8'); 43 | var content_lines = content.split(/\n/); 44 | content_lines[0] = content_lines[0].replace(/^(#!.*)$/, '// $1'); 45 | 46 | // TODO 47 | // content_lines.join('\n'), maybe new Buffer of that 48 | 49 | //Convert the Node module (CommonJS) to RequireJS. 50 | var require_re = /\brequire\(['"]([\w\d\-_\/\.]+?)['"]\)/g; 51 | 52 | // No idea why I'm doing this but it's cool. 53 | var match, dependencies = {}; 54 | while(match = require_re.exec(content)) 55 | dependencies[ match[1] ] = true; 56 | dependencies = Object.keys(dependencies); 57 | dependencies.forEach(function(dep) { 58 | //console.log(' depends: ' + dep); 59 | }) 60 | 61 | // In order to keep the error message line numbers correct, this makes an ugly final file. 62 | content = [ 'require.def(function(require, exports, module) {' 63 | //, 'var module = {};' 64 | //, 'var exports = {};' 65 | //, 'module.exports = exports;' 66 | , '' 67 | , content_lines.join('\n') 68 | , '; return(module.exports);' 69 | , '}); // define' 70 | ].join(''); 71 | //content = new Buffer(content); 72 | 73 | fs.writeFile(target, content, 'utf8', function(er) { 74 | if(er) return callback(er); 75 | return callback(); 76 | }) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Follow 6 | 7 | 8 |
Booting...
9 |
10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /browser/log4js.js: -------------------------------------------------------------------------------- 1 | // log4js stub 2 | // 3 | 4 | define(['querystring'], function(querystring) { 5 | function noop() {}; 6 | function trace(str) { return console.trace(str) } 7 | function log(str) { return console.log(str) } 8 | 9 | function is_verbose() { 10 | return !! querystring.parse(window.location.search).verbose; 11 | } 12 | 13 | function con(str) { 14 | var con = jQuery('#con'); 15 | var html = con.html(); 16 | con.html(html + str + "
"); 17 | } 18 | 19 | function debug(str) { 20 | if(is_verbose()) 21 | con(str); 22 | return console.debug(str); 23 | } 24 | 25 | function out(str) { 26 | con(str); 27 | log(str); 28 | } 29 | 30 | var err = out; 31 | 32 | var noops = { "trace": trace 33 | , "debug": debug 34 | , "info" : out 35 | , "warn" : err 36 | , "error": err 37 | , "fatal": err 38 | 39 | , "setLevel": noop 40 | } 41 | 42 | return function() { 43 | return { 'getLogger': function() { return noops } 44 | } 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /browser/main.js: -------------------------------------------------------------------------------- 1 | window.process = { env: {} }; 2 | 3 | if(!Object.keys) 4 | Object.keys = function(o){ 5 | if(typeof o !== 'object') 6 | throw new Error('Object.keys called on non-object: ' + JSON.stringify(o)); 7 | var ret=[], p; 8 | for (p in o) 9 | if(Object.prototype.hasOwnProperty.call(o,p)) 10 | ret.push(p); 11 | return ret; 12 | } 13 | 14 | if(!Array.isArray) 15 | Array.isArray = function(o) { 16 | return Object.prototype.toString.call(o) === '[object Array]'; 17 | } 18 | 19 | if(!Array.prototype.forEach) 20 | Array.prototype.forEach = function(callback) { 21 | var i, len = this.length; 22 | for(var i = 0; i < len; i++) 23 | callback(this[i], i, this); 24 | } 25 | 26 | if(!Array.prototype.reduce) 27 | Array.prototype.reduce = function(callback, state) { 28 | var i, len = this.length; 29 | for(i = 0; i < len; i++) 30 | state = callback(state, this[i]); 31 | return state; 32 | } 33 | 34 | if(!Array.prototype.filter) 35 | Array.prototype.filter = function(pred) { 36 | var i, len = this.length, result = []; 37 | for(i = 0; i < len; i++) 38 | if(!! pred(this[i])) 39 | result.push(this[i]); 40 | return result; 41 | } 42 | 43 | if(!Array.prototype.map) 44 | Array.prototype.map = function(func) { 45 | var i, len = this.length, result = []; 46 | for(i = 0; i < len; i++) 47 | result.push(func(this[i], i, this)); 48 | return result; 49 | } 50 | 51 | 52 | if(!window.console) 53 | window.console = {}; 54 | 55 | ; ['trace', 'debug', 'log', 'info', 'warn', 'error', 'fatal'].forEach(function(lev) { 56 | window.console[lev] = window.console[lev] || function() {}; 57 | }) 58 | 59 | define(['events', 'querystring', 'follow/cli'], function(events, querystring, cli) { 60 | var welcome = [ 'Starting Follow.' 61 | , 'Follow Follow' 62 | , 'at GitHub.' 63 | ]; 64 | jQuery('#boot').html('

' + welcome.join(' ') + '

'); 65 | 66 | // Set up some faux Node stuff. 67 | events.EventEmitter = events.EventEmitter2; 68 | var process = window.process = new events.EventEmitter; 69 | 70 | process.env = querystring.parse(window.location.search.slice(1)); 71 | 72 | process.stdout = {}; 73 | process.stdout.write = function(x) { 74 | var con = jQuery('#con'); 75 | con.append(x); 76 | } 77 | 78 | process.exit = function(code) { 79 | if(code === 0) 80 | console.log("'EXIT' " + code); 81 | else 82 | console.error("'EXIT' " + code); 83 | } 84 | 85 | 86 | return function() { // main() 87 | console.log('Main running'); 88 | 89 | try { cli.main() } 90 | catch(er) { console.log("Error starting tests"); console.log(er) } 91 | } 92 | }) 93 | -------------------------------------------------------------------------------- /browser/querystring.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | var exports = {}; 3 | 4 | exports.parse = function(str) { 5 | var result = {}; 6 | 7 | str = str || ""; 8 | str = str.replace(/^\?/, ""); 9 | if(!str) return result; 10 | 11 | var kvs = str.split('&'); 12 | kvs.forEach(function(pair) { 13 | var both = pair.split('='); 14 | result[both[0]] = both[1]; 15 | }) 16 | 17 | return result; 18 | } 19 | 20 | exports.stringify = function(query) { 21 | var result = []; 22 | for (var k in query) 23 | result.push(k + '=' + encodeURIComponent(query[k])); 24 | return result.join('&'); 25 | } 26 | 27 | return exports; 28 | }) 29 | -------------------------------------------------------------------------------- /browser/request.jquery.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var define = window.define 3 | if(!define) define = function(deps, definer) { 4 | if(!window.jQuery) 5 | throw new Error("Can't find jQuery"); 6 | return definer(window.jQuery); 7 | } 8 | 9 | define(['jquery'], function(jQuery) { 10 | 11 | // 12 | // request.jquery 13 | // 14 | 15 | var DEFAULT_TIMEOUT = 3 * 60 * 1000; // 3 minutes 16 | 17 | function request(options, callback) { 18 | var options_onResponse = options.onResponse; // Save this for later. 19 | 20 | if(typeof options === 'string') 21 | options = {'uri':options}; 22 | else 23 | options = JSON.parse(JSON.stringify(options)); // Use a duplicate for mutating. 24 | 25 | if(options.url) { 26 | options.uri = options.url; 27 | delete options.url; 28 | } 29 | 30 | if(!options.uri && options.uri !== "") 31 | throw new Error("options.uri is a required argument"); 32 | 33 | if(options.json) { 34 | options.body = JSON.stringify(options.json); 35 | delete options.json; 36 | } 37 | 38 | if(typeof options.uri != "string") 39 | throw new Error("options.uri must be a string"); 40 | 41 | ; ['proxy', '_redirectsFollowed', 'maxRedirects', 'followRedirect'].forEach(function(opt) { 42 | if(options[opt]) 43 | throw new Error("options." + opt + " is not supported"); 44 | }) 45 | 46 | options.method = options.method || 'GET'; 47 | options.headers = options.headers || {}; 48 | 49 | if(options.headers.host) 50 | throw new Error("Options.headers.host is not supported"); 51 | 52 | // onResponse is just like the callback but that is not quite what Node request does. 53 | callback = callback || options_onResponse; 54 | 55 | /* 56 | // Browsers do not like this. 57 | if(options.body) 58 | options.headers['content-length'] = options.body.length; 59 | */ 60 | 61 | var headers = {}; 62 | var beforeSend = function(xhr, settings) { 63 | if(!options.headers.authorization && options.auth) { 64 | debugger 65 | options.headers.authorization = 'Basic ' + b64_enc(options.auth.username + ':' + options.auth.password); 66 | } 67 | 68 | for (var key in options.headers) 69 | xhr.setRequestHeader(key, options.headers[key]); 70 | } 71 | 72 | // Establish a place where the callback arguments will go. 73 | var result = []; 74 | 75 | function fix_xhr(xhr) { 76 | var fixed_xhr = {}; 77 | for (var key in xhr) 78 | fixed_xhr[key] = xhr[key]; 79 | fixed_xhr.statusCode = xhr.status; 80 | return fixed_xhr; 81 | } 82 | 83 | var onSuccess = function(data, reason, xhr) { 84 | result = [null, fix_xhr(xhr), data]; 85 | } 86 | 87 | var onError = function (xhr, reason, er) { 88 | var body = undefined; 89 | 90 | if(reason == 'timeout') { 91 | er = er || new Error("Request timeout"); 92 | } else if(reason == 'error') { 93 | if(xhr.status > 299 && xhr.responseText.length > 0) { 94 | // Looks like HTTP worked, so there is no error as far as request is concerned. Simulate a success scenario. 95 | er = null; 96 | body = xhr.responseText; 97 | } 98 | } else { 99 | er = er || new Error("Unknown error; reason = " + reason); 100 | } 101 | 102 | result = [er, fix_xhr(xhr), body]; 103 | } 104 | 105 | var onComplete = function(xhr, reason) { 106 | if(result.length === 0) 107 | result = [new Error("Result does not exist at completion time")]; 108 | return callback && callback.apply(this, result); 109 | } 110 | 111 | 112 | var cors_creds = !!( options.creds || options.withCredentials ); 113 | 114 | return jQuery.ajax({ 'async' : true 115 | , 'cache' : (options.cache || false) 116 | , 'contentType': (options.headers['content-type'] || 'application/x-www-form-urlencoded') 117 | , 'type' : options.method 118 | , 'url' : options.uri 119 | , 'data' : (options.body || undefined) 120 | , 'timeout' : (options.timeout || request.DEFAULT_TIMEOUT) 121 | , 'dataType' : 'text' 122 | , 'processData': false 123 | , 'beforeSend' : beforeSend 124 | , 'success' : onSuccess 125 | , 'error' : onError 126 | , 'complete' : onComplete 127 | , 'xhrFields' : { 'withCredentials': cors_creds 128 | } 129 | }); 130 | 131 | }; 132 | 133 | request.withCredentials = false; 134 | request.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT; 135 | 136 | var shortcuts = [ 'get', 'put', 'post', 'head' ]; 137 | shortcuts.forEach(function(shortcut) { 138 | var method = shortcut.toUpperCase(); 139 | var func = shortcut.toLowerCase(); 140 | 141 | request[func] = function(opts) { 142 | if(typeof opts === 'string') 143 | opts = {'method':method, 'uri':opts}; 144 | else { 145 | opts = JSON.parse(JSON.stringify(opts)); 146 | opts.method = method; 147 | } 148 | 149 | var args = [opts].concat(Array.prototype.slice.apply(arguments, [1])); 150 | return request.apply(this, args); 151 | } 152 | }) 153 | 154 | request.json = function(options, callback) { 155 | options = JSON.parse(JSON.stringify(options)); 156 | options.headers = options.headers || {}; 157 | options.headers['accept'] = options.headers['accept'] || 'application/json'; 158 | 159 | if(options.method !== 'GET') 160 | options.headers['content-type'] = 'application/json'; 161 | 162 | return request(options, function(er, resp, body) { 163 | if(!er) 164 | body = JSON.parse(body) 165 | return callback && callback(er, resp, body); 166 | }) 167 | } 168 | 169 | request.couch = function(options, callback) { 170 | return request.json(options, function(er, resp, body) { 171 | if(er) 172 | return callback && callback(er, resp, body); 173 | 174 | if((resp.status < 200 || resp.status > 299) && body.error) 175 | // The body is a Couch JSON object indicating the error. 176 | return callback && callback(body, resp); 177 | 178 | return callback && callback(er, resp, body); 179 | }) 180 | } 181 | 182 | jQuery(document).ready(function() { 183 | jQuery.request = request; 184 | }) 185 | 186 | return request; 187 | 188 | }); 189 | 190 | // 191 | // Utility 192 | // 193 | 194 | // MIT License from http://phpjs.org/functions/base64_encode:358 195 | function b64_enc (data) { 196 | // Encodes string using MIME base64 algorithm 197 | var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 198 | var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc="", tmp_arr = []; 199 | 200 | if (!data) { 201 | return data; 202 | } 203 | 204 | // assume utf8 data 205 | // data = this.utf8_encode(data+''); 206 | 207 | do { // pack three octets into four hexets 208 | o1 = data.charCodeAt(i++); 209 | o2 = data.charCodeAt(i++); 210 | o3 = data.charCodeAt(i++); 211 | 212 | bits = o1<<16 | o2<<8 | o3; 213 | 214 | h1 = bits>>18 & 0x3f; 215 | h2 = bits>>12 & 0x3f; 216 | h3 = bits>>6 & 0x3f; 217 | h4 = bits & 0x3f; 218 | 219 | // use hexets to index into b64, and append result to encoded string 220 | tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); 221 | } while (i < data.length); 222 | 223 | enc = tmp_arr.join(''); 224 | 225 | switch (data.length % 3) { 226 | case 1: 227 | enc = enc.slice(0, -2) + '=='; 228 | break; 229 | case 2: 230 | enc = enc.slice(0, -1) + '='; 231 | break; 232 | } 233 | 234 | return enc; 235 | } 236 | 237 | })(); 238 | -------------------------------------------------------------------------------- /browser/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 0.26.0 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(){function M(a){return $.call(a)==="[object Function]"}function E(a){return $.call(a)==="[object Array]"}function V(a,c,g){for(var e in c)if(!(e in J)&&(!(e in a)||g))a[e]=c[e];return d}function R(a,c,d){a=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+a);if(d)a.originalError=d;return a}function aa(a,c,d){var e,x,j;for(e=0;j=c[e];e++){j=typeof j==="string"?{name:j}:j;x=j.location;if(d&&(!x||x.indexOf("/")!==0&&x.indexOf(":")===-1))x=d+"/"+(x||j.name);a[j.name]={name:j.name,location:x|| 8 | j.name,main:(j.main||"main").replace(fa,"").replace(ba,"")}}}function W(a,d){a.holdReady?a.holdReady(d):d?a.readyWait+=1:a.ready(!0)}function ga(a){function c(b,h){var n,o;if(b&&b.charAt(0)==="."&&h){p.pkgs[h]?h=[h]:(h=h.split("/"),h=h.slice(0,h.length-1));n=b=h.concat(b.split("/"));var a;for(o=0;a=n[o];o++)if(a===".")n.splice(o,1),o-=1;else if(a==="..")if(o===1&&(n[2]===".."||n[0]===".."))break;else o>0&&(n.splice(o-1,2),o-=2);o=p.pkgs[n=b[0]];b=b.join("/");o&&b===n+"/"+o.main&&(b=n)}return b}function g(b, 9 | h){var n=b?b.indexOf("!"):-1,o=null,a=h?h.name:null,ha=b,g,l;n!==-1&&(o=b.substring(0,n),b=b.substring(n+1,b.length));o&&(o=c(o,a));b&&(g=o?(n=m[o])?n.normalize?n.normalize(b,function(b){return c(b,a)}):c(b,a):"__$p"+a+"@"+(b||""):c(b,a),l=E[g],l||(l=d.toModuleUrl?d.toModuleUrl(f,g,h):f.nameToUrl(g,null,h),E[g]=l));return{prefix:o,name:g,parentMap:h,url:l,originalName:ha,fullName:o?o+"!"+(g||""):g}}function e(){var b=!0,h=p.priorityWait,n,a;if(h){for(a=0;n=h[a];a++)if(!s[n]){b=!1;break}b&&delete p.priorityWait}return b} 10 | function x(b){return function(h){b.exports=h}}function j(b,h,n){return function(){var a=[].concat(ia.call(arguments,0)),d;if(n&&M(d=a[a.length-1]))d.__requireJsBuild=!0;a.push(h);return b.apply(null,a)}}function q(b,h){var a=j(f.require,b,h);V(a,{nameToUrl:j(f.nameToUrl,b),toUrl:j(f.toUrl,b),defined:j(f.requireDefined,b),specified:j(f.requireSpecified,b),ready:d.ready,isBrowser:d.isBrowser});if(d.paths)a.paths=d.paths;return a}function v(b){var h=b.prefix,a=b.fullName;y[a]||a in m||(h&&!K[h]&&(K[h]= 11 | void 0,(S[h]||(S[h]=[])).push(b),(t[h]||(t[h]=[])).push({onDep:function(b){if(b===h){var a,n,d,c,f,e,j=S[h];if(j)for(d=0;a=j[d];d++)if(b=a.fullName,a=g(a.originalName,a.parentMap),a=a.fullName,n=t[b]||[],c=t[a],a!==b){b in y&&(delete y[b],y[a]=!0);t[a]=c?c.concat(n):n;delete t[b];for(c=0;c0)){if(p.priorityWait)if(e())G();else return;for(k in s)if(!(k in J)&&(c=!0,!s[k]))if(a)b+=k+" ";else{g=!0;break}if(c||f.waitCount){if(a&&b)return k=R("timeout","Load timeout for modules: "+b),k.requireType="timeout",k.requireModules= 16 | b,d.onError(k);if(g||f.scriptCount){if((B||ca)&&!X)X=setTimeout(function(){X=0;A()},50)}else{if(f.waitCount){for(H=0;b=I[H];H++)C(b,{});Y<5&&(Y+=1,A())}Y=0;d.checkReadyState()}}}}function D(b,a){var c=a.name,e=a.fullName,g;if(!(e in m||e in s))K[b]||(K[b]=m[b]),s[e]||(s[e]=!1),g=function(g){if(d.onPluginLoad)d.onPluginLoad(f,b,c,g);w({prefix:a.prefix,name:a.name,fullName:a.fullName,callback:function(){return g}});s[e]=!0},g.fromText=function(b,a){var c=N;f.loaded[b]=!1;f.scriptCount+=1;c&&(N=!1); 17 | d.exec(a);c&&(N=!0);f.completeLoad(b)},K[b].load(c,q(a.parentMap,!0),g,p)}function L(b){b.prefix&&b.name&&b.name.indexOf("__$p")===0&&m[b.prefix]&&(b=g(b.originalName,b.parentMap));var a=b.prefix,c=b.fullName,e=f.urlFetched;!y[c]&&!s[c]&&(y[c]=!0,a?m[a]?D(a,b):(O[a]||(O[a]=[],(t[a]||(t[a]=[])).push({onDep:function(b){if(b===a){for(var c,d=O[a],b=0;b0;m--)if(i=e.slice(0,m).join("/"),g[i]){e.splice(0,m,g[i]);break}else if(i=j[i]){b=b===i.name?i.location+"/"+i.main:i.location;e.splice(0,m,b);break}a=e.join("/")+(a||".js");a=(a.charAt(0)==="/"||a.match(/^\w+:/)?"":l.baseUrl)+a}return l.urlArgs?a+((a.indexOf("?")===-1?"?":"&")+l.urlArgs):a}};f.jQueryCheck=T;f.resume=G;return f}function la(){var a, 24 | c,d;if(C&&C.readyState==="interactive")return C;a=document.getElementsByTagName("script");for(c=a.length-1;c>-1&&(d=a[c]);c--)if(d.readyState==="interactive")return C=d;return null}var ma=/(\/\*([\s\S]*?)\*\/|\/\/(.*)$)/mg,na=/require\(\s*["']([^'"\s]+)["']\s*\)/g,fa=/^\.\//,ba=/\.js$/,$=Object.prototype.toString,q=Array.prototype,ia=q.slice,ka=q.splice,B=!!(typeof window!=="undefined"&&navigator&&document),ca=!B&&typeof importScripts!=="undefined",oa=B&&navigator.platform==="PLAYSTATION 3"?/^complete$/: 25 | /^(complete|loaded)$/,da=typeof opera!=="undefined"&&opera.toString()==="[object Opera]",ja="_r@@",J={},z={},U=[],C=null,Y=0,N=!1,d,q={},I,i,u,L,v,A,D,H,Q,ea,w,T,X;if(typeof define==="undefined"){if(typeof requirejs!=="undefined")if(M(requirejs))return;else q=requirejs,requirejs=void 0;typeof require!=="undefined"&&!M(require)&&(q=require,require=void 0);d=requirejs=function(a,c,d){var e="_",i;!E(a)&&typeof a!=="string"&&(i=a,E(c)?(a=c,c=d):a=[]);if(i&&i.context)e=i.context;d=z[e]||(z[e]=ga(e));i&& 26 | d.configure(i);return d.require(a,c)};d.config=function(a){return d(a)};typeof require==="undefined"&&(require=d);d.toUrl=function(a){return z._.toUrl(a)};d.version="0.26.0";d.isArray=E;d.isFunction=M;d.mixin=V;d.jsExtRegExp=/^\/|:|\?|\.js$/;i=d.s={contexts:z,skipAsync:{},isPageLoaded:!B,readyCalls:[]};if(d.isAsync=d.isBrowser=B)if(u=i.head=document.getElementsByTagName("head")[0],L=document.getElementsByTagName("base")[0])u=i.head=L.parentNode;d.onError=function(a){throw a;};d.load=function(a,c, 27 | g){var e=a.loaded;e[c]||(e[c]=!1);a.scriptCount+=1;d.attach(g,a,c);if(a.jQuery&&!a.jQueryIncremented)W(a.jQuery,!0),a.jQueryIncremented=!0};define=d.def=function(a,c,g){var e,i;typeof a!=="string"&&(g=c,c=a,a=null);d.isArray(c)||(g=c,c=[]);!a&&!c.length&&d.isFunction(g)&&g.length&&(g.toString().replace(ma,"").replace(na,function(a,d){c.push(d)}),c=(g.length===1?["require"]:["require","exports","module"]).concat(c));if(N&&(e=I||la()))a||(a=e.getAttribute("data-requiremodule")),i=z[e.getAttribute("data-requirecontext")]; 28 | (i?i.defQueue:U).push([a,c,g])};define.amd={multiversion:!0,plugins:!0,jQuery:!0};d.exec=function(a){return eval(a)};d.execCb=function(a,c,d,e){return c.apply(e,d)};d.onScriptLoad=function(a){var c=a.currentTarget||a.srcElement,g;if(a.type==="load"||oa.test(c.readyState))C=null,a=c.getAttribute("data-requirecontext"),g=c.getAttribute("data-requiremodule"),z[a].completeLoad(g),c.detachEvent&&!da?c.detachEvent("onreadystatechange",d.onScriptLoad):c.removeEventListener("load",d.onScriptLoad,!1)};d.attach= 29 | function(a,c,g,e,q){var j;if(B)return e=e||d.onScriptLoad,j=c&&c.config&&c.config.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),j.type=q||"text/javascript",j.charset="utf-8",j.async=!i.skipAsync[a],c&&j.setAttribute("data-requirecontext",c.contextName),j.setAttribute("data-requiremodule",g),j.attachEvent&&!da?(N=!0,j.attachEvent("onreadystatechange",e)):j.addEventListener("load",e,!1),j.src=a,I=j,L?u.insertBefore(j,L):u.appendChild(j), 30 | I=null,j;else if(ca)e=c.loaded,e[g]=!1,importScripts(a),c.completeLoad(g);return null};if(B){v=document.getElementsByTagName("script");for(H=v.length-1;H>-1&&(A=v[H]);H--){if(!u)u=A.parentNode;if(D=A.getAttribute("data-main")){if(!q.baseUrl)v=D.split("/"),A=v.pop(),v=v.length?v.join("/")+"/":"./",q.baseUrl=v,D=A.replace(ba,"");q.deps=q.deps?q.deps.concat(D):[D];break}}}i.baseUrl=q.baseUrl;d.pageLoaded=function(){if(!i.isPageLoaded){i.isPageLoaded=!0;Q&&clearInterval(Q);if(ea)document.readyState="complete"; 31 | d.callReady()}};d.checkReadyState=function(){var a=i.contexts,c;for(c in a)if(!(c in J)&&a[c].waitCount)return;i.isDone=!0;d.callReady()};d.callReady=function(){var a=i.readyCalls,c,d,e;if(i.isPageLoaded&&i.isDone){if(a.length){i.readyCalls=[];for(c=0;d=a[c];c++)d()}a=i.contexts;for(e in a)if(!(e in J)&&(c=a[e],c.jQueryIncremented))W(c.jQuery,!1),c.jQueryIncremented=!1}};d.ready=function(a){i.isPageLoaded&&i.isDone?a():i.readyCalls.push(a);return d};if(B){if(document.addEventListener){if(document.addEventListener("DOMContentLoaded", 32 | d.pageLoaded,!1),window.addEventListener("load",d.pageLoaded,!1),!document.readyState)ea=!0,document.readyState="loading"}else window.attachEvent&&(window.attachEvent("onload",d.pageLoaded),self===self.top&&(Q=setInterval(function(){try{document.body&&(document.documentElement.doScroll("left"),d.pageLoaded())}catch(a){}},30)));document.readyState==="complete"&&d.pageLoaded()}d(q);if(d.isAsync&&typeof setTimeout!=="undefined")w=i.contexts[q.context||"_"],w.requireWait=!0,setTimeout(function(){w.requireWait= 33 | !1;w.takeGlobalQueue();w.jQueryCheck();w.scriptCount||w.resume();d.checkReadyState()},0)}})(); 34 | -------------------------------------------------------------------------------- /browser/util.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | var exports = {}; 3 | 4 | exports.inspect = JSON.stringify; 5 | 6 | // Copy from Node 7 | /** 8 | * Inherit the prototype methods from one constructor into another. 9 | * 10 | * The Function.prototype.inherits from lang.js rewritten as a standalone 11 | * function (not on Function.prototype). NOTE: If this file is to be loaded 12 | * during bootstrapping this function needs to be revritten using some native 13 | * functions as prototype setup using normal JavaScript does not work as 14 | * expected during bootstrapping (see mirror.js in r114903). 15 | * 16 | * @param {function} ctor Constructor function which needs to inherit the 17 | * prototype. 18 | * @param {function} superCtor Constructor function to inherit prototype from. 19 | */ 20 | exports.inherits = function(ctor, superCtor) { 21 | ctor.super_ = superCtor; 22 | ctor.prototype = Object.create(superCtor.prototype, { 23 | constructor: { value: ctor, enumerable: false } 24 | }); 25 | }; 26 | 27 | return exports; 28 | }) 29 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // The follow command-line interface. 3 | // 4 | // Copyright 2011 Jason Smith, Jarrett Cruger and contributors 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | var lib = require('./lib') 19 | , couch_changes = require('./api') 20 | ; 21 | 22 | function puts(str) { 23 | process.stdout.write(str + "\n"); 24 | } 25 | 26 | function main() { 27 | var db = require.isBrowser ? (process.env.db || '/_users') : process.argv[2]; 28 | puts('Watching: ' + db); 29 | 30 | var feed = new couch_changes.Feed(); 31 | feed.db = db; 32 | feed.since = (process.env.since === 'now') ? 'now' : parseInt(process.env.since || '0'); 33 | 34 | feed.heartbeat = (process.env.heartbeat || '3000').replace(/s$/, '000'); 35 | feed.heartbeat = parseInt(feed.heartbeat); 36 | 37 | if(require.isBrowser) 38 | feed.feed = 'longpoll'; 39 | if(process.env.host) 40 | feed.headers.host = process.env.host; 41 | if(process.env.inactivity) 42 | feed.inactivity_ms = parseInt(process.env.inactivity); 43 | if(process.env.limit) 44 | feed.limit = parseInt(process.env.limit); 45 | 46 | feed.query_params.pid = process.pid; 47 | feed.filter = process.env.filter || example_filter; 48 | function example_filter(doc, req) { 49 | // This is a local filter. It runs on the client side. 50 | var label = 'Filter ' + (req.query.pid || '::'); 51 | 52 | if(process.env.show_doc) 53 | console.log(label + ' doc: ' + JSON.stringify(doc)); 54 | if(process.env.show_req) 55 | console.log(label + ' for ' + doc._id + ' req: ' + JSON.stringify(req)); 56 | return true; 57 | } 58 | 59 | feed.on('confirm', function() { 60 | puts('Database confirmed: ' + db); 61 | }) 62 | 63 | feed.on('change', function(change) { 64 | puts('Change:' + JSON.stringify(change)); 65 | }) 66 | 67 | feed.on('timeout', function(state) { 68 | var seconds = state.elapsed_ms / 1000; 69 | var hb = state.heartbeat / 1000; 70 | puts('Timeout after ' + seconds + 's inactive, heartbeat=' + hb + 's'); 71 | }) 72 | 73 | feed.on('retry', function(state) { 74 | if(require.isBrowser) 75 | puts('Long polling since ' + state.since); 76 | else 77 | puts('Retry since ' + state.since + ' after ' + state.after + 'ms'); 78 | }) 79 | 80 | feed.on('response', function() { 81 | puts('Streaming response:'); 82 | }) 83 | 84 | feed.on('error', function(er) { 85 | //console.error(er); 86 | console.error('Changes error ============\n' + er.stack); 87 | setTimeout(function() { process.exit(0) }, 100); 88 | }) 89 | 90 | process.on('uncaughtException', function(er) { 91 | puts('========= UNCAUGHT EXCEPTION; This is bad'); 92 | puts(er.stack); 93 | setTimeout(function() { process.exit(1) }, 100); 94 | }) 95 | 96 | feed.follow(); 97 | } 98 | 99 | exports.main = main; 100 | if(!require.isBrowser && process.argv[1] == module.filename) 101 | main(); 102 | -------------------------------------------------------------------------------- /lib/feed.js: -------------------------------------------------------------------------------- 1 | // Core routines for event emitters 2 | // 3 | // Copyright 2011 Jason Smith, Jarrett Cruger and contributors 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | var lib = require('../lib') 18 | , util = require('util') 19 | , events = require('events') 20 | , request = require('request') 21 | , Changes = require('./stream').Changes 22 | , querystring = require('querystring') 23 | 24 | // Use the library timeout functions, primarily so the test suite can catch errors. 25 | var setTimeout = lib.setTimeout 26 | , clearTimeout = lib.clearTimeout 27 | 28 | var DEFAULT_HEARTBEAT = 30000; 29 | var HEARTBEAT_TIMEOUT_COEFFICIENT = 1.25; // E.g. heartbeat 1000ms would trigger a timeout after 1250ms of no heartbeat. 30 | var DEFAULT_MAX_RETRY_SECONDS = 60 * 60; 31 | var INITIAL_RETRY_DELAY = 1000; 32 | var RESPONSE_GRACE_TIME = 5000; 33 | 34 | var FEED_PARAMETERS = ['since', 'limit', 'feed', 'heartbeat', 'filter', 'include_docs', 'view', 'style', 'conflicts', 'attachments', 'att_encoding_info']; 35 | 36 | var EventEmitter = events.EventEmitter2 || events.EventEmitter; 37 | 38 | 39 | util.inherits(Feed, EventEmitter); 40 | function Feed (opts) { 41 | var self = this; 42 | EventEmitter.call(self); 43 | 44 | opts = opts || {} 45 | 46 | self.feed = 'continuous'; 47 | self.heartbeat = opts.heartbeat || DEFAULT_HEARTBEAT; 48 | self.max_retry_seconds = opts.max_retry_seconds || DEFAULT_MAX_RETRY_SECONDS; 49 | self.inactivity_ms = null; 50 | self.initial_retry_delay = opts.initial_retry_delay || INITIAL_RETRY_DELAY; 51 | self.response_grace_time = opts.response_grace_time || RESPONSE_GRACE_TIME; 52 | 53 | self.headers = {}; 54 | self.request = opts.request || {} // Extra options for potentially future versions of request. The caller can supply them. 55 | 56 | self.since = 0; 57 | self.is_paused = false 58 | self.caught_up = false 59 | self.retry_delay = self.initial_retry_delay; 60 | 61 | self.query_params = {}; // Extra `req.query` values for filter functions 62 | 63 | if(typeof opts === 'string') 64 | opts = {'db': opts}; 65 | 66 | Object.keys(opts).forEach(function(key) { 67 | if (typeof self[key] !== 'function') 68 | self[key] = opts[key]; 69 | }) 70 | 71 | self.pending = { request : null 72 | , activity_at : null 73 | }; 74 | } // Feed 75 | 76 | Feed.prototype.start = 77 | Feed.prototype.follow = function follow_feed() { 78 | var self = this; 79 | 80 | self.db = self.db || self.url || self.uri 81 | delete self.url 82 | delete self.uri 83 | 84 | if(!self.db) 85 | throw new Error('Database URL required'); 86 | 87 | if (self.db.match(/\/_db_updates$/)) 88 | self.is_db_updates = true; 89 | 90 | if(self.is_db_updates) 91 | delete self.since; 92 | 93 | if(self.feed !== 'continuous' && self.feed !== 'longpoll') 94 | throw new Error('The only valid feed options are "continuous" and "longpoll"'); 95 | 96 | if(typeof self.heartbeat !== 'number') 97 | throw new Error('Required "heartbeat" value'); 98 | 99 | self.log = lib.log4js.getLogger(self.db); 100 | self.log.setLevel(process.env.follow_log_level || "info"); 101 | 102 | self.emit('start'); 103 | return self.confirm(); 104 | } 105 | 106 | Feed.prototype.confirm = function confirm_feed() { 107 | var self = this; 108 | 109 | self.db_safe = lib.scrub_creds(self.db); 110 | 111 | var endpoint = self.is_db_updates ? 'server' : 'database'; 112 | 113 | self.log.debug('Checking ' + endpoint + ': ' + self.db_safe); 114 | 115 | var confirm_timeout = self.heartbeat * 3; // Give it time to look up the name, connect, etc. 116 | var timeout_id = setTimeout(function() { 117 | return self.die(new Error('Timeout confirming ' + endpoint + ': ' + self.db_safe)); 118 | }, confirm_timeout); 119 | 120 | var headers = lib.JP(lib.JS(self.headers)); 121 | headers.accept = 'application/json'; 122 | 123 | var uri = self.is_db_updates ? self.db.replace(/\/_db_updates$/, '') : self.db; 124 | var req = {'uri':uri, 'headers':headers} 125 | Object.keys(self.request).forEach(function(key) { 126 | req[key] = self.request[key]; 127 | }) 128 | 129 | req = request(req, db_response) 130 | self.emit('confirm_request', req) 131 | 132 | function db_response(er, resp, body) { 133 | clearTimeout(timeout_id); 134 | 135 | if(er) 136 | return self.die(er); 137 | 138 | var db; 139 | try { 140 | db = JSON.parse(body) 141 | } catch(json_er) { 142 | return self.emit('error', json_er) 143 | } 144 | 145 | if(!self.is_db_updates && !self.dead && (!db.db_name || !db.instance_start_time)) 146 | return self.emit('error', new Error('Bad DB response: ' + body)); 147 | 148 | if(self.is_db_updates && !self.dead && !db.couchdb) 149 | return self.emit('error', new Error('Bad server response: ' + body)); 150 | 151 | if (!self.is_db_updates) 152 | self.original_db_seq = db.update_seq 153 | 154 | self.log.debug('Confirmed ' + endpoint + ': ' + self.db_safe); 155 | self.emit('confirm', db); 156 | 157 | if(self.since == 'now') { 158 | self.log.debug('Query since "now" is the same as query since -1') 159 | self.since = -1 160 | } 161 | 162 | if(self.since == -1) { 163 | self.log.debug('Query since '+self.since+' will start at ' + db.update_seq) 164 | self.since = db.update_seq 165 | } else if(self.since < 0) { 166 | if(isNaN(db.update_seq)) 167 | return self.emit('error', new Error('DB requires specific id in "since"')); 168 | 169 | self.log.debug('Query since '+self.since+' will start at ' + (db.update_seq + self.since + 1)) 170 | self.since = db.update_seq + self.since + 1 171 | } 172 | 173 | // If the next change would come after the current update_seq, just fake a catchup event now. 174 | if(self.original_db_seq == self.since) { 175 | self.caught_up = true 176 | self.emit('catchup', db.update_seq) 177 | } 178 | 179 | return self.query(); 180 | } 181 | } 182 | 183 | Feed.prototype.query = function query_feed() { 184 | var self = this; 185 | 186 | var query_params = JSON.parse(JSON.stringify(self.query_params)); 187 | 188 | FEED_PARAMETERS.forEach(function(key) { 189 | if(key in self) 190 | query_params[key] = self[key]; 191 | }) 192 | 193 | if(typeof query_params.filter !== 'string') 194 | delete query_params.filter; 195 | 196 | if(typeof self.filter === 'function' && !query_params.include_docs) { 197 | self.log.debug('Enabling include_docs for client-side filter'); 198 | query_params.include_docs = true; 199 | } 200 | 201 | // Limit the response size for longpoll. 202 | var poll_size = 100; 203 | if(query_params.feed == 'longpoll' && (!query_params.limit || query_params.limit > poll_size)) 204 | query_params.limit = poll_size; 205 | 206 | var feed_url = self.db + (self.is_db_updates ? '' : '/_changes') + '?' + querystring.stringify(query_params); 207 | 208 | self.headers.accept = self.headers.accept || 'application/json'; 209 | var req = { method : 'GET' 210 | , uri : feed_url 211 | , headers: self.headers 212 | , encoding: 'utf-8' 213 | } 214 | 215 | req.changes_query = query_params; 216 | Object.keys(self.request).forEach(function(key) { 217 | req[key] = self.request[key]; 218 | }) 219 | 220 | var now = new Date 221 | , feed_ts = lib.JDUP(now) 222 | , feed_id = process.env.follow_debug ? feed_ts.match(/\.(\d\d\d)Z$/)[1] : feed_ts 223 | 224 | self.log.debug('Feed query ' + feed_id + ': ' + lib.scrub_creds(feed_url)) 225 | var feed_request = request(req) 226 | 227 | feed_request.on('response', function(res) { 228 | self.log.debug('Remove feed from agent pool: ' + feed_id) 229 | feed_request.req.socket.emit('agentRemove') 230 | 231 | // Simulate the old onResponse option. 232 | on_feed_response(null, res, res.body) 233 | }) 234 | 235 | feed_request.on('error', on_feed_response) 236 | 237 | // The response headers must arrive within one heartbeat. 238 | var response_timer = setTimeout(response_timed_out, self.heartbeat + self.response_grace_time) 239 | , timed_out = false 240 | 241 | return self.emit('query', feed_request) 242 | 243 | function response_timed_out() { 244 | self.log.debug('Feed response timed out: ' + feed_id) 245 | timed_out = true 246 | return self.retry() 247 | } 248 | 249 | function on_feed_response(er, resp, body) { 250 | clearTimeout(response_timer) 251 | 252 | if((resp !== undefined && resp.body) || body) 253 | return self.die(new Error('Cannot handle a body in the feed response: ' + lib.JS(resp.body || body))) 254 | 255 | if(timed_out) { 256 | self.log.debug('Ignoring late response: ' + feed_id); 257 | return destroy_response(resp); 258 | } 259 | 260 | if(er) { 261 | self.log.debug('Request error ' + feed_id + ': ' + er.stack); 262 | destroy_response(resp); 263 | return self.retry(); 264 | } 265 | 266 | if (resp.statusCode === 404) { 267 | destroy_response(resp); 268 | self.log.warn('Database not found. Stopping changes feed.'); 269 | var del_er = new Error('Database not found.'); 270 | del_er.deleted = true; 271 | del_er.last_seq = self.since; 272 | return self.die(del_er); 273 | } 274 | 275 | if(resp.statusCode !== 200) { 276 | self.log.debug('Bad changes response ' + feed_id + ': ' + resp.statusCode); 277 | destroy_response(resp); 278 | return self.retry(); 279 | } 280 | 281 | self.log.debug('Good response: ' + feed_id); 282 | self.retry_delay = self.initial_retry_delay; 283 | 284 | self.emit('response', resp); 285 | 286 | var changes_stream = new Changes 287 | changes_stream.log = lib.log4js.getLogger('stream ' + self.db) 288 | changes_stream.log.setLevel(self.log.level.levelStr) 289 | changes_stream.feed = self.feed 290 | feed_request.pipe(changes_stream) 291 | 292 | changes_stream.created_at = now 293 | changes_stream.id = function() { return feed_id } 294 | return self.prep(changes_stream) 295 | } 296 | } 297 | 298 | Feed.prototype.prep = function prep_request(changes_stream) { 299 | var self = this; 300 | 301 | var now = new Date; 302 | self.pending.request = changes_stream; 303 | self.pending.activity_at = now; 304 | self.pending.wait_timer = null; 305 | 306 | // Just re-run the pause or resume to do the needful on changes_stream (self.pending.request). 307 | if(self.is_paused) 308 | self.pause() 309 | else 310 | self.resume() 311 | 312 | // The inactivity timer is for time between *changes*, or time between the 313 | // initial connection and the first change. Therefore it goes here. 314 | self.change_at = now; 315 | if(self.inactivity_ms) { 316 | clearTimeout(self.inactivity_timer); 317 | self.inactivity_timer = setTimeout(function() { self.on_inactivity() }, self.inactivity_ms); 318 | } 319 | 320 | changes_stream.on('heartbeat', handler_for('heartbeat')) 321 | changes_stream.on('error', handler_for('error')) 322 | changes_stream.on('data', handler_for('data')) 323 | changes_stream.on('end', handler_for('end')) 324 | 325 | return self.wait(); 326 | 327 | function handler_for(ev) { 328 | var name = 'on_couch_' + ev; 329 | var inner_handler = self[name]; 330 | 331 | return handle_confirmed_req_event; 332 | function handle_confirmed_req_event() { 333 | if(self.pending.request === changes_stream) 334 | return inner_handler.apply(self, arguments); 335 | 336 | if(!changes_stream.created_at) 337 | return self.die(new Error("Received data from unknown request")); // Pretty sure this is impossible. 338 | 339 | var s_to_now = (new Date() - changes_stream.created_at) / 1000; 340 | var s_to_req = '[no req]'; 341 | if(self.pending.request) 342 | s_to_req = (self.pending.request.created_at - changes_stream.created_at) / 1000; 343 | 344 | var msg = ': ' + changes_stream.id() + ' to_req=' + s_to_req + 's, to_now=' + s_to_now + 's'; 345 | 346 | if(ev == 'end' || ev == 'data' || ev == 'heartbeat') { 347 | self.log.debug('Old "' + ev + '": ' + changes_stream.id()) 348 | return destroy_req(changes_stream) 349 | } 350 | 351 | self.log.warn('Old "'+ev+'"' + msg); 352 | } 353 | } 354 | } 355 | 356 | Feed.prototype.wait = function wait_for_event() { 357 | var self = this; 358 | self.emit('wait'); 359 | 360 | if(self.pending.wait_timer) 361 | return self.die(new Error('wait() called but there is already a wait_timer: ' + self.pending.wait_timer)); 362 | 363 | var timeout_ms = self.heartbeat * HEARTBEAT_TIMEOUT_COEFFICIENT; 364 | var req_id = self.pending.request && self.pending.request.id() 365 | var msg = 'Req ' + req_id + ' timeout=' + timeout_ms; 366 | if(self.inactivity_ms) 367 | msg += ', inactivity=' + self.inactivity_ms; 368 | msg += ': ' + self.db_safe; 369 | 370 | self.log.debug(msg); 371 | self.pending.wait_timer = setTimeout(function() { self.on_timeout() }, timeout_ms); 372 | } 373 | 374 | Feed.prototype.got_activity = function() { 375 | var self = this 376 | 377 | if (self.dead) 378 | return 379 | 380 | // 381 | // We may not have a wait_timer so just clear it and null it out if it does 382 | // exist 383 | // 384 | clearTimeout(self.pending.wait_timer) 385 | self.pending.wait_timer = null 386 | self.pending.activity_at = new Date 387 | } 388 | 389 | 390 | Feed.prototype.pause = function() { 391 | var self = this 392 | , was_paused = self.is_paused 393 | 394 | // Emit pause after pausing the stream, to allow listeners to react. 395 | self.is_paused = true 396 | if(self.pending && self.pending.request && self.pending.request.pause) 397 | self.pending.request.pause() 398 | else 399 | self.log.warn('No pending request to pause') 400 | 401 | if(!was_paused) 402 | self.emit('pause') 403 | } 404 | 405 | Feed.prototype.resume = function() { 406 | var self = this 407 | , was_paused = self.is_paused 408 | 409 | // Emit resume before resuming the data feed, to allow listeners to prepare. 410 | self.is_paused = false 411 | if(was_paused) 412 | self.emit('resume') 413 | 414 | if(self.pending && self.pending.request && self.pending.request.resume) 415 | self.pending.request.resume() 416 | else 417 | self.log.warn('No pending request to resume') 418 | } 419 | 420 | 421 | Feed.prototype.on_couch_heartbeat = function on_couch_heartbeat() { 422 | var self = this 423 | 424 | self.got_activity() 425 | if(self.dead) 426 | return self.log.debug('Skip heartbeat processing for dead feed') 427 | 428 | self.emit('heartbeat') 429 | 430 | if(self.dead) 431 | return self.log.debug('No wait: heartbeat listener stopped this feed') 432 | self.wait() 433 | } 434 | 435 | Feed.prototype.on_couch_data = function on_couch_data(change) { 436 | var self = this; 437 | self.log.debug('Data from ' + self.pending.request.id()); 438 | 439 | self.got_activity() 440 | if(self.dead) 441 | return self.log.debug('Skip data processing for dead feed') 442 | 443 | // The changes stream guarantees that this data is valid JSON. 444 | change = JSON.parse(change) 445 | 446 | //self.log.debug('Object:\n' + util.inspect(change)); 447 | if ('last_seq' in change) { 448 | self.log.debug('Found last_seq in change: retrying request'); 449 | self.since = change.last_seq; 450 | return self.retry(); 451 | } 452 | 453 | if(!self.is_db_updates && !change.seq) 454 | return self.die(new Error('Change has no .seq field: ' + JSON.stringify(change))) 455 | 456 | self.on_change(change) 457 | 458 | // on_change() might work its way all the way to a "change" event, and the listener 459 | // might call .stop(), which means among other things that no more events are desired. 460 | // The die() code sets a self.dead flag to indicate this. 461 | if(self.dead) 462 | return self.log.debug('No wait: change listener stopped this feed') 463 | self.wait() 464 | } 465 | 466 | Feed.prototype.on_timeout = function on_timeout() { 467 | var self = this; 468 | if (self.dead) 469 | return self.log.debug('No timeout: change listener stopped this feed'); 470 | 471 | self.log.debug('Timeout') 472 | 473 | var now = new Date; 474 | var elapsed_ms = now - self.pending.activity_at; 475 | 476 | self.emit('timeout', {elapsed_ms:elapsed_ms, heartbeat:self.heartbeat, id:self.pending.request.id()}); 477 | 478 | /* 479 | var msg = ' for timeout after ' + elapsed_ms + 'ms; heartbeat=' + self.heartbeat; 480 | if(!self.pending.request.id) 481 | self.log.warn('Closing req (no id) ' + msg + ' req=' + util.inspect(self.pending.request)); 482 | else 483 | self.log.warn('Closing req ' + self.pending.request.id() + msg); 484 | */ 485 | 486 | destroy_req(self.pending.request); 487 | self.retry() 488 | } 489 | 490 | Feed.prototype.retry = function retry() { 491 | var self = this; 492 | 493 | clearTimeout(self.pending.wait_timer); 494 | self.pending.wait_timer = null; 495 | 496 | self.log.debug('Retry since=' + self.since + ' after ' + self.retry_delay + 'ms ') 497 | self.emit('retry', {since:self.since, after:self.retry_delay, db:self.db_safe}); 498 | 499 | self.retry_timer = setTimeout(function() { self.query() }, self.retry_delay); 500 | 501 | var max_retry_ms = self.max_retry_seconds * 1000; 502 | self.retry_delay *= 2; 503 | if(self.retry_delay > max_retry_ms) 504 | self.retry_delay = max_retry_ms; 505 | } 506 | 507 | Feed.prototype.on_couch_end = function on_couch_end() { 508 | var self = this; 509 | 510 | self.log.debug('Changes feed ended ' + self.pending.request.id()); 511 | self.pending.request = null; 512 | return self.retry(); 513 | } 514 | 515 | Feed.prototype.on_couch_error = function on_couch_error(er) { 516 | var self = this; 517 | 518 | self.log.debug('Changes query eror: ' + lib.JS(er.stack)); 519 | return self.retry(); 520 | } 521 | 522 | Feed.prototype.stop = function(val) { 523 | var self = this 524 | self.log.debug('Stop') 525 | 526 | // Die with no errors. 527 | self.die() 528 | self.emit('stop', val); 529 | } 530 | 531 | Feed.prototype.die = function(er) { 532 | var self = this; 533 | 534 | if(er) 535 | self.log.fatal('Fatal error: ' + er.stack); 536 | 537 | // Warn code executing later that death has occured. 538 | self.dead = true 539 | 540 | clearTimeout(self.retry_timer) 541 | clearTimeout(self.inactivity_timer) 542 | clearTimeout(self.pending.wait_timer) 543 | 544 | self.inactivity_timer = null 545 | self.pending.wait_timer = null 546 | 547 | var req = self.pending.request; 548 | self.pending.request = null; 549 | if(req) { 550 | self.log.debug('Destroying req ' + req.id()); 551 | destroy_req(req); 552 | } 553 | 554 | if(er) 555 | self.emit('error', er); 556 | } 557 | 558 | Feed.prototype.on_change = function on_change(change) { 559 | var self = this; 560 | 561 | if(!self.is_db_updates && !change.seq) 562 | return self.die(new Error('No seq value in change: ' + lib.JS(change))); 563 | 564 | if(!self.is_db_updates && change.seq == self.since) { 565 | self.log.debug('Bad seq value ' + change.seq + ' since=' + self.since); 566 | return destroy_req(self.pending.request); 567 | } 568 | 569 | if(typeof self.filter !== 'function') 570 | return self.on_good_change(change); 571 | 572 | var req = lib.JDUP({'query': self.pending.request.changes_query}); 573 | var filter_args; 574 | 575 | if (self.is_db_updates) { 576 | if(!change.db_name || !change.type) 577 | return self.die(new Error('Internal _db_updates filter needs .db_name and .type in change ', change)); 578 | filter_args = [change.db_name, change.type, req]; 579 | } else { 580 | if(!change.doc) 581 | return self.die(new Error('Internal filter needs .doc in change ' + change.seq)); 582 | 583 | // Don't let the filter mutate the real data. 584 | var doc = lib.JDUP(change.doc); 585 | filter_args = [doc, req]; 586 | } 587 | var result = false; 588 | try { 589 | result = self.filter.apply(null, filter_args); 590 | } catch (er) { 591 | self.log.debug('Filter error', er); 592 | } 593 | 594 | result = (result && true) || false; 595 | if(result) { 596 | self.log.debug('Builtin filter PASS for change: ' + change.seq); 597 | return self.on_good_change(change); 598 | } else { 599 | self.log.debug('Builtin filter FAIL for change: ' + change.seq); 600 | 601 | // Even with a filtered change, a "catchup" event might still be appropriate. 602 | self.check_for_catchup(change.seq) 603 | } 604 | } 605 | 606 | Feed.prototype.on_good_change = function on_good_change(change) { 607 | var self = this; 608 | 609 | if(self.inactivity_ms && !self.inactivity_timer) 610 | return self.die(new Error('Cannot find inactivity timer during change')); 611 | 612 | clearTimeout(self.inactivity_timer); 613 | self.inactivity_timer = null; 614 | if(self.inactivity_ms) 615 | self.inactivity_timer = setTimeout(function() { self.on_inactivity() }, self.inactivity_ms); 616 | 617 | self.change_at = new Date; 618 | 619 | if(!self.is_db_updates) 620 | self.since = change.seq; 621 | 622 | self.emit('change', change); 623 | 624 | self.check_for_catchup(change.seq) 625 | } 626 | 627 | Feed.prototype.check_for_catchup = function check_for_catchup(seq) { 628 | var self = this 629 | 630 | if (self.is_db_updates) 631 | return 632 | if(self.caught_up) 633 | return 634 | if(seq < self.original_db_seq) 635 | return 636 | 637 | self.caught_up = true 638 | self.emit('catchup', seq) 639 | } 640 | 641 | Feed.prototype.on_inactivity = function on_inactivity() { 642 | var self = this; 643 | var now = new Date; 644 | var elapsed_ms = now - self.change_at; 645 | var elapsed_s = elapsed_ms / 1000; 646 | 647 | // 648 | // Since this is actually not fatal, lets just totally reset and start a new 649 | // request, JUST in case something was bad. 650 | // 651 | self.log.debug('Req ' + self.pending.request.id() + ' made no changes for ' + elapsed_s + 's'); 652 | return self.restart(); 653 | 654 | } 655 | 656 | Feed.prototype.restart = function restart() { 657 | var self = this 658 | 659 | self.emit('restart') 660 | 661 | // Kill ourselves and then start up once again 662 | self.stop() 663 | self.dead = false 664 | self.start() 665 | } 666 | 667 | module.exports = { "Feed" : Feed 668 | }; 669 | 670 | 671 | /* 672 | * Utilities 673 | */ 674 | 675 | function destroy_req(req) { 676 | if(req) 677 | destroy_response(req.response) 678 | 679 | if(req && typeof req.destroy == 'function') 680 | req.destroy() 681 | } 682 | 683 | function destroy_response(response) { 684 | if(!response) 685 | return; 686 | 687 | if(typeof response.abort === 'function') 688 | response.abort(); 689 | 690 | if(response.connection) 691 | response.connection.destroy(); 692 | } 693 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Jason Smith, Jarrett Cruger and contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | exports.scrub_creds = function scrub_creds(url) { 16 | return url.replace(/^(https?:\/\/)[^:]+:[^@]+@(.*)$/, '$1$2'); // Scrub username and password 17 | } 18 | 19 | exports.JP = JSON.parse; 20 | exports.JS = JSON.stringify; 21 | exports.JDUP = function(obj) { return JSON.parse(JSON.stringify(obj)) }; 22 | 23 | var timeouts = { 'setTimeout': setTimeout 24 | , 'clearTimeout': clearTimeout 25 | } 26 | 27 | exports.setTimeout = function() { return timeouts.setTimeout.apply(this, arguments) } 28 | exports.clearTimeout = function() { return timeouts.clearTimeout.apply(this, arguments) } 29 | exports.timeouts = function(set, clear) { 30 | timeouts.setTimeout = set 31 | timeouts.clearTimeout = clear 32 | } 33 | 34 | var debug = require('debug') 35 | 36 | function getLogger(name) { 37 | return { "trace": noop 38 | , "debug": debug('follow:' + name + ':debug') 39 | , "info" : debug('follow:' + name + ':info') 40 | , "warn" : debug('follow:' + name + ':warn') 41 | , "error": debug('follow:' + name + ':error') 42 | , "fatal": debug('follow:' + name + ':fatal') 43 | 44 | , "level": {'level':0, 'levelStr':'noop'} 45 | , "setLevel": noop 46 | } 47 | } 48 | 49 | function noop () {} 50 | 51 | exports.log4js = { 'getLogger': getLogger } 52 | -------------------------------------------------------------------------------- /lib/stream.js: -------------------------------------------------------------------------------- 1 | // Changes stream 2 | // 3 | // Copyright 2011 Jason Smith, Jarrett Cruger and contributors 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | var lib = require('../lib') 18 | , util = require('util') 19 | , stream = require('stream') 20 | , request = require('request') 21 | 22 | // Use the library timeout functions, primarily so the test suite can catch errors. 23 | var setTimeout = lib.setTimeout 24 | , clearTimeout = lib.clearTimeout 25 | 26 | 27 | var DEFS = 28 | { 'longpoll_header': '{"results":[' 29 | , 'log_level' : process.env.follow_log_level || 'info' 30 | } 31 | 32 | module.exports = { 'Changes': Changes 33 | } 34 | 35 | 36 | util.inherits(Changes, stream) 37 | function Changes (opts) { 38 | var self = this 39 | stream.call(self) 40 | 41 | self.readable = true 42 | self.writable = true 43 | 44 | self.headers = {} 45 | self.statusCode = null 46 | 47 | opts = opts || {} 48 | self.feed = opts.feed || null // "continuous" or "longpoll" 49 | self.encoding = opts.encoding || 'utf8' 50 | 51 | self.log = opts.log 52 | if(!self.log) { 53 | self.log = lib.log4js.getLogger('change_stream') 54 | self.log.setLevel(DEFS.log_level) 55 | } 56 | 57 | self.is_sending = true 58 | self.is_ending = false 59 | self.is_dead = false 60 | 61 | self.source = null 62 | self.expect = null 63 | self.buf = null 64 | self.changes = [] 65 | 66 | self.on('pipe', function(src) { 67 | if(!self.source) 68 | self.source = src 69 | else { 70 | var er = new Error('Already have a pipe source') 71 | er.source = self.source 72 | self.error(er) 73 | } 74 | }) 75 | } 76 | 77 | 78 | Changes.prototype.setHeader = function(key, val) { 79 | var self = this 80 | self.headers[key] = val 81 | } 82 | 83 | // 84 | // Readable stream API 85 | // 86 | 87 | Changes.prototype.setEncoding = function(encoding) { 88 | var self = this 89 | self.encoding = encoding // TODO 90 | } 91 | 92 | 93 | Changes.prototype.pause = function() { 94 | var self = this 95 | self.is_sending = false 96 | 97 | if(self.source && self.source.pause) 98 | self.source.pause() 99 | } 100 | 101 | 102 | Changes.prototype.resume = function() { 103 | var self = this 104 | self.is_sending = true 105 | if(self.source && self.source.resume) 106 | self.source.resume() 107 | self.emit_changes() 108 | } 109 | 110 | // 111 | // Writable stream API 112 | // 113 | 114 | Changes.prototype.write = function(data, encoding) { 115 | var self = this 116 | 117 | data = self.normalize_data(data, encoding) 118 | if(typeof data != 'string') 119 | return // Looks like normalize_data emitted an error. 120 | 121 | if(self.feed === 'longpoll') 122 | return self.write_longpoll(data) 123 | else if(self.feed === 'continuous') 124 | return self.write_continuous(data) 125 | } 126 | 127 | 128 | Changes.prototype.write_longpoll = function(data) { 129 | var self = this 130 | 131 | if(self.buf === null) 132 | self.buf = [] 133 | 134 | self.buf.push(data) 135 | return true 136 | } 137 | 138 | 139 | Changes.prototype.write_continuous = function(data) { 140 | var self = this 141 | 142 | var offset, json, change 143 | , buf = (self.buf || "") + data 144 | 145 | self.log.debug('write: ' + util.inspect({'data':data, 'buf':buf})) 146 | 147 | // Buf could have 0, 1, or many JSON objects in it. 148 | while((offset = buf.indexOf("\n")) >= 0) { 149 | json = buf.substr(0, offset); 150 | buf = buf.substr(offset + 1); 151 | self.log.debug('JSON: ' + util.inspect(json)) 152 | 153 | // Heartbeats (empty strings) are fine, but otherwise confirm valid JSON. 154 | if(json === "") 155 | ; 156 | 157 | else if(json[0] !== '{') 158 | return self.error(new Error('Non-object JSON data: ' + json)) 159 | 160 | else { 161 | try { change = JSON.parse(json) } 162 | catch (er) { return self.error(er) } 163 | 164 | self.log.debug('Object: ' + util.inspect(change)) 165 | json = JSON.stringify(change) 166 | } 167 | 168 | // Change (or heartbeat) looks good. 169 | self.changes.push(json) 170 | } 171 | 172 | // Remember the unused data and send all known good changes (or heartbeats). The data (or heartbeat) 173 | // event listeners may call .pause() so remember the is_sending state now before calling them. 174 | var was_sending = self.is_sending 175 | self.buf = buf 176 | self.emit_changes() 177 | return was_sending 178 | } 179 | 180 | 181 | Changes.prototype.end = function(data, encoding) { 182 | var self = this 183 | 184 | self.is_ending = true 185 | self.writable = false 186 | 187 | // Always call write, even with no data, so it can fire the "end" event. 188 | self.write(data, encoding) 189 | 190 | if(self.feed === 'longpoll') { 191 | var changes = [ DEFS.longpoll_header ].concat(self.buf).join('') 192 | try { changes = JSON.parse(changes) || {} } 193 | catch (er) { return self.error(er) } 194 | 195 | if(!Array.isArray(changes.results)) 196 | return self.error(new Error('No "results" field in feed')) 197 | if(self.changes.length !== 0) 198 | return self.error(new Error('Changes are already queued: ' + JSON.stringify(self.changes))) 199 | 200 | self.changes = changes.results.map(function(change) { return JSON.stringify(change) }) 201 | return self.emit_changes() 202 | } 203 | 204 | else if(self.feed === 'continuous') { 205 | if(self.buf !== "") 206 | self.log.debug('Unprocessed data after "end" called: ' + util.inspect(self.buf)) 207 | } 208 | } 209 | 210 | 211 | Changes.prototype.emit_changes = function() { 212 | var self = this 213 | 214 | while(self.is_sending && self.changes.length > 0) { 215 | var change = self.changes.shift() 216 | if(change === "") { 217 | self.log.debug('emit: heartbeat') 218 | self.emit('heartbeat') 219 | } 220 | 221 | else { 222 | self.log.debug('emit: data') 223 | self.emit('data', change) 224 | } 225 | } 226 | 227 | if(self.is_sending && self.is_ending && self.changes.length === 0) { 228 | self.is_ending = false 229 | self.readable = false 230 | self.log.debug('emit: end') 231 | self.emit('end') 232 | } 233 | } 234 | 235 | // 236 | // Readable/writable stream API 237 | // 238 | 239 | Changes.prototype.destroy = function() { 240 | var self = this 241 | self.log.debug('destroy') 242 | 243 | self.is_dead = true 244 | self.is_ending = false 245 | self.is_sending = false 246 | 247 | if(self.source && typeof self.source.abort == 'function') 248 | return self.source.abort() 249 | 250 | if(self.source && typeof self.source.destroy === 'function') 251 | self.source.destroy() 252 | 253 | // Often the source is from the request package, so destroy its response object. 254 | if(self.source && self.source.__isRequestRequest && self.source.response 255 | && typeof self.source.response.destroy === 'function') 256 | self.source.response.destroy() 257 | } 258 | 259 | 260 | Changes.prototype.destroySoon = function() { 261 | var self = this 262 | throw new Error('not implemented') 263 | //return self.request.destroySoon() 264 | } 265 | 266 | // 267 | // Internal implementation 268 | // 269 | 270 | Changes.prototype.normalize_data = function(data, encoding) { 271 | var self = this 272 | 273 | if(data instanceof Buffer) 274 | data = data.toString(encoding) 275 | else if(typeof data === 'undefined' && typeof encoding === 'undefined') 276 | data = "" 277 | 278 | if(typeof data != 'string') 279 | return self.error(new Error('Not a string or Buffer: ' + util.inspect(data))) 280 | 281 | if(self.feed !== 'continuous' && self.feed !== 'longpoll') 282 | return self.error(new Error('Must set .feed to "continuous" or "longpoll" before writing data')) 283 | 284 | if(self.expect === null) 285 | self.expect = (self.feed == 'longpoll') 286 | ? DEFS.longpoll_header 287 | : "" 288 | 289 | var prefix = data.substr(0, self.expect.length) 290 | data = data.substr(prefix.length) 291 | 292 | var expected_part = self.expect.substr(0, prefix.length) 293 | , expected_remainder = self.expect.substr(expected_part.length) 294 | 295 | if(prefix !== expected_part) 296 | return self.error(new Error('Prefix not expected '+util.inspect(expected_part)+': ' + util.inspect(prefix))) 297 | 298 | self.expect = expected_remainder 299 | return data 300 | } 301 | 302 | 303 | Changes.prototype.error = function(er) { 304 | var self = this 305 | 306 | self.readable = false 307 | self.writable = false 308 | self.emit('error', er) 309 | 310 | // The write() method sometimes returns this value, so if there was an error, make write() return false. 311 | return false 312 | } 313 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "follow", 3 | "version": "1.1.0", 4 | "author": { 5 | "name": "Jason Smith", 6 | "email": "jason.h.smith@gmail.com" 7 | }, 8 | "contributors": [ 9 | "Jarrett Cruger " 10 | ], 11 | "description": "Extremely robust, fault-tolerant CouchDB changes follower", 12 | "license": "Apache-2.0", 13 | "keywords": [ 14 | "couchdb", 15 | "changes", 16 | "sleep", 17 | "sleepy" 18 | ], 19 | "homepage": "http://github.com/jhs/follow", 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/jhs/follow" 23 | }, 24 | "engines": { 25 | "node": ">=0.8.0" 26 | }, 27 | "dependencies": { 28 | "browser-request": "~0.3.0", 29 | "debug": "^2.1.0", 30 | "request": "^2.83.0" 31 | }, 32 | "devDependencies": { 33 | "tap": "~0.4.0", 34 | "traceback": "~0.3.0" 35 | }, 36 | "browser": { 37 | "request": "browser-request" 38 | }, 39 | "main": "./api.js", 40 | "scripts": { 41 | "test": "tap test/*.js" 42 | }, 43 | "bin": { 44 | "follow": "./cli.js" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/couch.js: -------------------------------------------------------------------------------- 1 | // CouchDB tests 2 | // 3 | // This module is also a library for other test modules. 4 | 5 | var tap = require('tap') 6 | , util = require('util') 7 | , assert = require('assert') 8 | , request = require('request') 9 | 10 | var follow = require('../api') 11 | , DB = process.env.db || 'http://localhost:5984/follow_test' 12 | , DB_UPDATES = process.env.db_updates || 'http://localhost:5984/_db_updates' 13 | , RTT = null 14 | 15 | 16 | module.exports = { 'DB': DB 17 | , 'DB_UPDATES': DB_UPDATES 18 | , 'rtt' : get_rtt 19 | , 'redo': redo_couch 20 | , 'setup': setup_test 21 | , 'make_data': make_data 22 | , 'create_and_delete_db': create_and_delete_db 23 | } 24 | 25 | 26 | function get_rtt() { 27 | if(!RTT) 28 | throw new Error('RTT was not set. Use setup(test) or redo(callback)') 29 | return RTT 30 | } 31 | 32 | 33 | // Basically a redo but testing along the way. 34 | function setup_test(test_func) { 35 | assert.equal(typeof test_func, 'function', 'Please provide tap.test function') 36 | 37 | test_func('Initialize CouchDB', function(t) { 38 | init_db(t, function(er, rtt) { 39 | RTT = rtt 40 | t.end() 41 | }) 42 | }) 43 | } 44 | 45 | function redo_couch(callback) { 46 | function noop() {} 47 | var t = { 'ok':noop, 'false':noop, 'equal':noop, 'end':noop } 48 | init_db(t, function(er, rtt) { 49 | if(rtt) 50 | RTT = rtt 51 | return callback(er) 52 | }) 53 | } 54 | 55 | function init_db(t, callback) { 56 | var create_begin = new Date 57 | 58 | request.del({uri:DB, json:true}, function(er, res) { 59 | t.false(er, 'Clear old test DB: ' + DB) 60 | t.ok(!res.body.error || res.body.error == 'not_found', 'Couch cleared old test DB: ' + DB) 61 | 62 | request.put({uri:DB, json:true}, function(er, res) { 63 | t.false(er, 'Create new test DB: ' + DB) 64 | t.false(res.body.error, 'Couch created new test DB: ' + DB) 65 | 66 | var values = ['first', 'second', 'third'] 67 | , stores = 0 68 | values.forEach(function(val) { 69 | var doc = { _id:'doc_'+val, value:val } 70 | 71 | request.post({uri:DB, json:doc}, function(er, res) { 72 | t.false(er, 'POST document') 73 | t.equal(res.statusCode, 201, 'Couch stored test document') 74 | 75 | stores += 1 76 | if(stores == values.length) { 77 | var rtt = (new Date) - create_begin 78 | callback(null, rtt) 79 | //request.post({uri:DB, json:{_id:'_local/rtt', ms:(new Date)-begin}}, function(er, res) { 80 | // t.false(er, 'Store RTT value') 81 | // t.equal(res.statusCode, 201, 'Couch stored RTT value') 82 | // t.end() 83 | //}) 84 | } 85 | }) 86 | }) 87 | }) 88 | }) 89 | } 90 | 91 | function create_and_delete_db(t, callback) { 92 | request.put({ uri: DB + 1, json: true}, function (er, res) { 93 | t.false(er, 'create test db'); 94 | request.del({uri: DB +1, json: true}, function (er, res) { 95 | t.false(er, 'Clear old test DB: ' + DB) 96 | t.ok(!res.body.error); 97 | callback(); 98 | }); 99 | }); 100 | } 101 | 102 | 103 | function make_data(minimum_size, callback) { 104 | var payload = {'docs':[]} 105 | , size = 0 106 | 107 | // TODO: Make document number 20 really large, at least over 9kb. 108 | while(size < minimum_size) { 109 | var doc = {} 110 | , key_count = rndint(0, 25) 111 | 112 | while(key_count-- > 0) 113 | doc[rndstr(8)] = rndstr(20) 114 | 115 | // The 20th document has one really large string value. 116 | if(payload.docs.length == 19) { 117 | var big_str = rndstr(9000, 15000) 118 | doc.big = {'length':big_str.length, 'value':big_str} 119 | } 120 | 121 | size += JSON.stringify(doc).length // This is an underestimate because an _id and _rev will be added. 122 | payload.docs.push(doc) 123 | } 124 | 125 | request.post({'uri':DB+'/_bulk_docs', 'json':payload}, function(er, res, body) { 126 | if(er) throw er 127 | 128 | if(res.statusCode != 201) 129 | throw new Error('Bad bulk_docs update: ' + util.inspect(res.body)) 130 | 131 | if(res.body.length != payload.docs.length) 132 | throw new Error('Should have results for '+payload.docs.length+' doc insertions') 133 | 134 | body.forEach(function(result) { 135 | if(!result || !result.id || !result.rev) 136 | throw new Error('Bad bulk_docs response: ' + util.inspect(result)) 137 | }) 138 | 139 | return callback(payload.docs.length) 140 | }) 141 | 142 | function rndstr(minlen, maxlen) { 143 | if(!maxlen) { 144 | maxlen = minlen 145 | minlen = 1 146 | } 147 | 148 | var str = "" 149 | , length = rndint(minlen, maxlen) 150 | 151 | while(length-- > 0) 152 | str += String.fromCharCode(rndint(97, 122)) 153 | 154 | return str 155 | } 156 | 157 | function rndint(min, max) { 158 | return min + Math.floor(Math.random() * (max - min + 1)) 159 | } 160 | } 161 | 162 | 163 | if(require.main === module) 164 | setup_test(tap.test) 165 | -------------------------------------------------------------------------------- /test/follow.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap') 2 | , test = tap.test 3 | , util = require('util') 4 | , request = require('request') 5 | 6 | var couch = require('./couch') 7 | , follow = require('../api') 8 | 9 | 10 | couch.setup(test) 11 | 12 | test('Follow API', function(t) { 13 | var i = 0 14 | , saw = {} 15 | 16 | var feed = follow(couch.DB, function(er, change) { 17 | t.is(this, feed, 'Callback "this" value is the feed object') 18 | 19 | i += 1 20 | t.false(er, 'No error coming back from follow: ' + i) 21 | t.equal(change.seq, i, 'Change #'+i+' should have seq_id='+i) 22 | saw[change.id] = true 23 | 24 | if(i == 3) { 25 | t.ok(saw.doc_first, 'Got the first document') 26 | t.ok(saw.doc_second, 'Got the second document') 27 | t.ok(saw.doc_third , 'Got the third document') 28 | 29 | t.doesNotThrow(function() { feed.stop() }, 'No problem calling stop()') 30 | 31 | t.end() 32 | } 33 | }) 34 | }) 35 | 36 | test("Confirmation request behavior", function(t) { 37 | var feed = follow(couch.DB, function() {}) 38 | 39 | var confirm_req = null 40 | , follow_req = null 41 | 42 | feed.on('confirm_request', function(req) { confirm_req = req }) 43 | feed.on('query', function(req) { follow_req = req }) 44 | 45 | setTimeout(check_req, couch.rtt() * 2) 46 | function check_req() { 47 | t.ok(confirm_req, 'The confirm_request event should have fired by now') 48 | t.ok(confirm_req.agent, 'The confirm request has an agent') 49 | 50 | t.ok(follow_req, 'The follow_request event should have fired by now') 51 | t.ok(follow_req.agent, 'The follow request has an agent') 52 | 53 | // Confirm that the changes follower is not still in the pool. 54 | var host = 'localhost:5984' 55 | var sockets = follow_req.req.agent.sockets[host] || [] 56 | sockets.forEach(function(socket, i) { 57 | t.isNot(socket, follow_req.req.connection, 'The changes follower is not socket '+i+' in the agent pool') 58 | }) 59 | 60 | feed.stop() 61 | t.end() 62 | } 63 | }) 64 | 65 | test('Heartbeats', function(t) { 66 | t.ok(couch.rtt(), 'The couch RTT is known') 67 | var check_time = couch.rtt() * 3.5 // Enough time for 3 heartbeats. 68 | 69 | var beats = 0 70 | , retries = 0 71 | 72 | var feed = follow(couch.DB, function() {}) 73 | feed.heartbeat = couch.rtt() 74 | feed.on('response', function() { feed.retry_delay = 1 }) 75 | 76 | feed.on('heartbeat', function() { beats += 1 }) 77 | feed.on('retry', function() { retries += 1 }) 78 | 79 | feed.on('catchup', function() { 80 | t.equal(beats, 0, 'Still 0 heartbeats after receiving changes') 81 | t.equal(retries, 0, 'Still 0 retries after receiving changes') 82 | 83 | //console.error('Waiting ' + couch.rtt() + ' * 3 = ' + check_time + ' to check stuff') 84 | setTimeout(check_counters, check_time) 85 | function check_counters() { 86 | t.equal(beats, 3, 'Three heartbeats ('+couch.rtt()+') fired after '+check_time+' ms') 87 | t.equal(retries, 0, 'No retries after '+check_time+' ms') 88 | 89 | feed.stop() 90 | t.end() 91 | } 92 | }) 93 | }) 94 | 95 | test('Catchup events', function(t) { 96 | t.ok(couch.rtt(), 'The couch RTT is known') 97 | 98 | var feed = follow(couch.DB, function() {}) 99 | var last_seen = 0 100 | 101 | feed.on('change', function(change) { last_seen = change.seq }) 102 | feed.on('catchup', function(id) { 103 | t.equal(last_seen, 3, 'The catchup event fires after the change event that triggered it') 104 | t.equal(id , 3, 'The catchup event fires on the seq_id of the latest change') 105 | feed.stop() 106 | t.end() 107 | }) 108 | }) 109 | 110 | test('Data due on a paused feed', function(t) { 111 | t.ok(couch.rtt(), 'The couch RTT is known') 112 | var HB = couch.rtt() * 5 113 | , HB_DUE = HB * 1.25 // This comes from the hard-coded constant in feed.js. 114 | 115 | var events = [] 116 | , last_event = new Date 117 | 118 | function ev(type) { 119 | var now = new Date 120 | events.push({'elapsed':now - last_event, 'event':type}) 121 | last_event = now 122 | } 123 | 124 | var feed = follow(couch.DB, function() {}) 125 | feed.heartbeat = HB 126 | feed.on('response', function() { feed.retry_delay = 1 }) 127 | // TODO 128 | // feed.pause() 129 | 130 | feed.on('heartbeat', function() { ev('heartbeat') }) 131 | feed.on('change' , function() { ev('change') }) 132 | feed.on('timeout' , function() { ev('timeout') }) 133 | feed.on('retry' , function() { ev('retry') }) 134 | feed.on('pause' , function() { ev('pause') }) 135 | feed.on('resume' , function() { ev('resume') }) 136 | feed.on('stop' , function() { ev('stop') }) 137 | 138 | feed.on('change', function(change) { 139 | if(change.seq == 1) { 140 | feed.pause() 141 | // Stay paused long enough for three heartbeats to be overdue. 142 | setTimeout(function() { feed.resume() }, HB_DUE * 3 * 1.1) 143 | } 144 | 145 | if(change.seq == 3) { 146 | feed.stop() 147 | setTimeout(check_results, couch.rtt()) 148 | } 149 | }) 150 | 151 | function check_results() { 152 | console.error('rtt=%j HB=%j', couch.rtt(), HB) 153 | events.forEach(function(event, i) { console.error('Event %d: %j', i, event) }) 154 | 155 | t.equal(events[0].event, 'change', 'First event was a change') 156 | t.ok(events[0].elapsed < couch.rtt(), 'First change came quickly') 157 | 158 | t.equal(events[1].event, 'pause', 'Second event was a pause, reacting to the first change') 159 | t.ok(events[1].elapsed < 10, 'Pause called/fired immediately after the change') 160 | 161 | t.equal(events[2].event, 'timeout', 'Third event was the first timeout') 162 | t.ok(percent(events[2].elapsed, HB_DUE) > 95, 'First timeout fired when the heartbeat was due') 163 | 164 | t.equal(events[3].event, 'retry', 'Fourth event is a retry') 165 | t.ok(events[3].elapsed < 10, 'First retry fires immediately after the first timeout') 166 | 167 | t.equal(events[4].event, 'timeout', 'Fifth event was the second timeout') 168 | t.ok(percent(events[4].elapsed, HB_DUE) > 95, 'Second timeout fired when the heartbeat was due') 169 | 170 | t.equal(events[5].event, 'retry', 'Sixth event is a retry') 171 | t.ok(events[5].elapsed < 10, 'Second retry fires immediately after the second timeout') 172 | 173 | t.equal(events[6].event, 'timeout', 'Seventh event was the third timeout') 174 | t.ok(percent(events[6].elapsed, HB_DUE) > 95, 'Third timeout fired when the heartbeat was due') 175 | 176 | t.equal(events[7].event, 'retry', 'Eighth event is a retry') 177 | t.ok(events[7].elapsed < 10, 'Third retry fires immediately after the third timeout') 178 | 179 | t.equal(events[8].event, 'resume', 'Ninth event resumed the feed') 180 | 181 | t.equal(events[9].event, 'change', 'Tenth event was the second change') 182 | t.ok(events[9].elapsed < 10, 'Second change came immediately after resume') 183 | 184 | t.equal(events[10].event, 'change', 'Eleventh event was the third change') 185 | t.ok(events[10].elapsed < 10, 'Third change came immediately after the second change') 186 | 187 | t.equal(events[11].event, 'stop', 'Twelfth event was the feed stopping') 188 | t.ok(events[11].elapsed < 10, 'Stop event came immediately in response to the third change') 189 | 190 | t.notOk(events[12], 'No thirteenth event') 191 | 192 | return t.end() 193 | } 194 | 195 | function percent(a, b) { 196 | var num = Math.min(a, b) 197 | , denom = Math.max(a, b) 198 | return 100 * num / denom 199 | } 200 | }) 201 | 202 | test('Events for DB confirmation and hitting the original seq', function(t) { 203 | t.plan(7) 204 | var feed = follow(couch.DB, on_change) 205 | 206 | var events = { 'confirm':null } 207 | feed.on('confirm', function(db) { events.confirm = db }) 208 | feed.on('catchup', caught_up) 209 | 210 | // This will run 3 times. 211 | function on_change(er, ch) { 212 | t.false(er, 'No problem with the feed') 213 | if(ch.seq == 3) { 214 | t.ok(events.confirm, 'Confirm event fired') 215 | t.equal(events.confirm && events.confirm.db_name, 'follow_test', 'Confirm event returned the Couch DB object') 216 | t.equal(events.confirm && events.confirm.update_seq, 3, 'Confirm event got the update_seq right') 217 | } 218 | } 219 | 220 | function caught_up(seq) { 221 | t.equal(seq, 3, 'Catchup event fired on update 3') 222 | 223 | feed.stop() 224 | t.end() 225 | } 226 | }) 227 | 228 | test('Handle a deleted database', function(t) { 229 | var feed = follow(couch.DB, function(er, change) { 230 | if(er) 231 | return t.equal(er.last_seq, 3, 'Got an error for the deletion event') 232 | 233 | if(change.seq < 3) 234 | return 235 | 236 | t.equal(change.seq, 3, 'Got change number 3') 237 | 238 | var redo_er 239 | couch.redo(function(er) { redo_er = er }) 240 | 241 | setTimeout(check_results, couch.rtt() * 2) 242 | function check_results() { 243 | t.false(er, 'No problem redoing the couch') 244 | t.end() 245 | } 246 | }) 247 | }) 248 | 249 | test('Follow _db_updates', function (t) { 250 | t.plan(4); 251 | var count = 0; 252 | var types = ['created']; 253 | var db_name = couch.DB.split('/').slice(-1)[0]; 254 | var feed = new follow.Feed({db: couch.DB_UPDATES}); 255 | feed.on('error', function (error) { 256 | t.false(error, 'Error in feed ' + error); 257 | }); 258 | 259 | feed.on('change', function (change) { 260 | t.ok(change, 'Received a change ' + JSON.stringify(change)); 261 | t.equal(change.type, types[count], 'Change should be of type "' + types[count] + '"'); 262 | t.equal(change.db_name, db_name + 1, 'Change should have db_name with the name of the db where the change occoured.'); 263 | feed.stop(); 264 | }); 265 | 266 | feed.start(); 267 | couch.create_and_delete_db(t, function () { 268 | t.ok(true, 'things happened'); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /test/issues.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap') 2 | , test = tap.test 3 | , util = require('util') 4 | , request = require('request') 5 | , traceback = require('traceback') 6 | 7 | var lib = require('../lib') 8 | , couch = require('./couch') 9 | , follow = require('../api') 10 | 11 | couch.setup(test) 12 | 13 | test('Issue #5', function(t) { 14 | var saw = { loops:0, seqs:{} } 15 | 16 | var saw_change = false 17 | // -2 means I want to see the last change. 18 | var feed = follow({'db':couch.DB, since:-2}, function(er, change) { 19 | t.equal(change.seq, 3, 'Got the latest change, 3') 20 | t.false(saw_change, 'Only one callback run for since=-2 (assuming no subsequent change') 21 | saw_change = true 22 | 23 | process.nextTick(function() { feed.stop() }) 24 | feed.on('stop', function() { 25 | // Test using since=-1 (AKA since="now"). 26 | follow({'db':couch.DB, since:'now'}, function(er, change) { 27 | t.equal(change.seq, 4, 'Only get changes made after starting the feed') 28 | t.equal(change.id, "You're in now, now", 'Got the subsequent change') 29 | 30 | this.stop() 31 | t.end() 32 | }) 33 | 34 | // Let that follower settle in, then send it something 35 | setTimeout(function() { 36 | var doc = { _id:"You're in now, now", movie:"Spaceballs" } 37 | request.post({uri:couch.DB, json:doc}, function(er) { 38 | if(er) throw er 39 | }) 40 | }, couch.rtt()) 41 | }) 42 | }) 43 | }) 44 | 45 | couch.setup(test) // Back to the expected documents 46 | 47 | test('Issue #6', function(t) { 48 | // When we see change 1, delete the database. The rest should still come in, then the error indicating deletion. 49 | var saw = { seqs:{}, redid:false, redo_err:null } 50 | 51 | follow(couch.DB, function(er, change) { 52 | if(!er) { 53 | saw.seqs[change.seq] = true 54 | t.notOk(change.last_seq, 'Change '+change.seq+' ha no .last_seq') 55 | if(change.seq == 1) { 56 | couch.redo(function(er) { 57 | saw.redid = true 58 | saw.redo_err = er 59 | }) 60 | } 61 | } 62 | 63 | else setTimeout(function() { 64 | // Give the redo time to finish, then confirm that everything happened as expected. 65 | // Hopefully this error indicates the database was deleted. 66 | t.ok(er.message.match(/deleted .* 3$/), 'Got delete error after change 3') 67 | t.ok(er.deleted, 'Error object indicates database deletion') 68 | t.equal(er.last_seq, 3, 'Error object indicates the last change number') 69 | 70 | t.ok(saw.seqs[1], 'Change 1 was processed') 71 | t.ok(saw.seqs[2], 'Change 2 was processed') 72 | t.ok(saw.seqs[3], 'Change 3 was processed') 73 | t.ok(saw.redid, 'The redo function ran') 74 | t.false(saw.redo_err, 'No problem redoing the database') 75 | 76 | return t.end() 77 | }, couch.rtt() * 2) 78 | }) 79 | }) 80 | 81 | test('Issue #8', function(t) { 82 | var timeouts = timeout_tracker() 83 | 84 | // Detect inappropriate timeouts after the run. 85 | var runs = {'set':false, 'clear':false} 86 | function badSetT() { 87 | runs.set = true 88 | return setTimeout.apply(this, arguments) 89 | } 90 | 91 | function badClearT() { 92 | runs.clear = true 93 | return clearTimeout.apply(this, arguments) 94 | } 95 | 96 | follow(couch.DB, function(er, change) { 97 | t.false(er, 'Got a feed') 98 | t.equal(change.seq, 1, 'Handler only runs for one change') 99 | 100 | this.on('stop', check_timeouts) 101 | this.stop() 102 | 103 | function check_timeouts() { 104 | t.equal(timeouts().length, 0, 'No timeouts by the time stop fires') 105 | 106 | lib.timeouts(badSetT, badClearT) 107 | 108 | // And give it a moment to try something bad. 109 | setTimeout(final_timeout_check, 250) 110 | function final_timeout_check() { 111 | t.equal(timeouts().length, 0, 'No lingering timeouts after teardown: ' + tims(timeouts())) 112 | t.false(runs.set, 'No more setTimeouts ran') 113 | t.false(runs.clear, 'No more clearTimeouts ran') 114 | 115 | t.end() 116 | } 117 | } 118 | }) 119 | }) 120 | 121 | test('Issue #9', function(t) { 122 | var timeouts = timeout_tracker() 123 | 124 | follow({db:couch.DB, inactivity_ms:30000}, function(er, change) { 125 | if(change.seq == 1) 126 | return // Let it run through once, just for fun. 127 | 128 | t.equal(change.seq, 2, 'The second change will be the last') 129 | this.stop() 130 | 131 | setTimeout(check_inactivity_timer, 250) 132 | function check_inactivity_timer() { 133 | t.equal(timeouts().length, 0, 'No lingering timeouts after teardown: ' + tims(timeouts())) 134 | timeouts().forEach(function(id) { clearTimeout(id) }) 135 | t.end() 136 | } 137 | }) 138 | }) 139 | 140 | // 141 | // Utilities 142 | // 143 | 144 | function timeout_tracker() { 145 | // Return an array tracking in-flight timeouts. 146 | var timeouts = [] 147 | var set_num = 0 148 | 149 | lib.timeouts(set, clear) 150 | return function() { return timeouts } 151 | 152 | function set() { 153 | var result = setTimeout.apply(this, arguments) 154 | 155 | var caller = traceback()[2] 156 | set_num += 1 157 | result.caller = '('+set_num+') ' + (caller.method || caller.name || '') + ' in ' + caller.file + ':' + caller.line 158 | //console.error('setTimeout: ' + result.caller) 159 | 160 | timeouts.push(result) 161 | //console.error('inflight ('+timeouts.length+'): ' + tims(timeouts)) 162 | return result 163 | } 164 | 165 | function clear(id) { 166 | //var caller = traceback()[2] 167 | //caller = (caller.method || caller.name || '') + ' in ' + caller.file + ':' + caller.line 168 | //console.error('clearTimeout: ' + (id && id.caller) + ' <- ' + caller) 169 | 170 | timeouts = timeouts.filter(function(element) { return element !== id }) 171 | //console.error('inflight ('+timeouts.length+'): ' + tims(timeouts)) 172 | return clearTimeout.apply(this, arguments) 173 | } 174 | } 175 | 176 | function tims(arr) { 177 | return JSON.stringify(arr.map(function(timer) { return timer.caller })) 178 | } 179 | -------------------------------------------------------------------------------- /test/issues/10.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap') 2 | , test = tap.test 3 | , util = require('util') 4 | 5 | // Issue #10 is about missing log4js. This file sets the environment variable to disable it. 6 | process.env.log_plain = true 7 | 8 | var lib = require('../../lib') 9 | , couch = require('../couch') 10 | , follow = require('../../api') 11 | 12 | couch.setup(test) 13 | 14 | test('Issue #10', function(t) { 15 | follow({db:couch.DB, inactivity_ms:30000}, function(er, change) { 16 | console.error('Change: ' + JSON.stringify(change)) 17 | if(change.seq == 2) 18 | this.stop() 19 | 20 | this.on('stop', function() { 21 | t.end() 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/issues/43.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap') 2 | var test = tap.test 3 | var util = require('util') 4 | 5 | var lib = require('../../lib') 6 | var couch = require('../couch') 7 | var follow = require('../../api') 8 | 9 | couch.setup(test) 10 | 11 | test('Issue #43', function(t) { 12 | var changes = 0 13 | 14 | var feed = follow({'db':couch.DB, 'inactivity_ms':couch.rtt()*3}, on_change) 15 | feed.on('stop', on_stop) 16 | feed.on('error', on_err) 17 | 18 | function on_err(er) { 19 | t.false(er, 'Error event should never fire') 20 | t.end() 21 | } 22 | 23 | function on_change(er, change) { 24 | changes += 1 25 | this.stop() 26 | } 27 | 28 | function on_stop(er) { 29 | t.false(er, 'No problem stopping') 30 | t.equal(changes, 1, 'Only one change seen since stop was called in its callback') 31 | t.end() 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /test/stream.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap') 2 | , test = tap.test 3 | , util = require('util') 4 | , request = require('request') 5 | 6 | var couch = require('./couch') 7 | , follow = require('../api') 8 | 9 | 10 | couch.setup(test) 11 | 12 | test('The Changes stream API', function(t) { 13 | var feed = new follow.Changes 14 | 15 | t.type(feed.statusCode, 'null', 'Changes has a .statusCode (initially null)') 16 | t.type(feed.setHeader, 'function', 'Changes has a .setHeader() method') 17 | t.type(feed.headers, 'object', 'Changes has a .headers object') 18 | t.same(feed.headers, {}, 'Changes headers are initially empty') 19 | 20 | t.end() 21 | }) 22 | 23 | test('Readable Stream API', function(t) { 24 | var feed = new follow.Changes 25 | 26 | t.is(feed.readable, true, 'Changes is a readable stream') 27 | 28 | t.type(feed.setEncoding, 'function', 'Changes has .setEncoding() method') 29 | t.type(feed.pause, 'function', 'Changes has .pause() method') 30 | t.type(feed.resume, 'function', 'Changes has .resume() method') 31 | t.type(feed.destroy, 'function', 'Changes has .destroy() method') 32 | t.type(feed.destroySoon, 'function', 'Changes has .destroySoon() method') 33 | t.type(feed.pipe, 'function', 'Changes has .pipe() method') 34 | 35 | t.end() 36 | }) 37 | 38 | test('Writatable Stream API', function(t) { 39 | var feed = new follow.Changes 40 | 41 | t.is(feed.writable, true, 'Changes is a writable stream') 42 | 43 | t.type(feed.write, 'function', 'Changes has .write() method') 44 | t.type(feed.end, 'function', 'Changes has .end() method') 45 | t.type(feed.destroy, 'function', 'Changes has .destroy() method') 46 | t.type(feed.destroySoon, 'function', 'Changes has .destroySoon() method') 47 | 48 | t.end() 49 | }) 50 | 51 | test('Error conditions', function(t) { 52 | var feed = new follow.Changes 53 | t.throws(write, 'Throw if the feed type is not defined') 54 | 55 | feed = new follow.Changes 56 | feed.feed = 'neither longpoll nor continuous' 57 | t.throws(write, 'Throw if the feed type is not longpoll nor continuous') 58 | 59 | feed = new follow.Changes({'feed':'longpoll'}) 60 | t.throws(write('stuff'), 'Throw if the "results" line is not sent first') 61 | 62 | feed = new follow.Changes({'feed':'longpoll'}) 63 | t.doesNotThrow(write('') , 'Empty string is fine waiting for "results"') 64 | t.doesNotThrow(write('{') , 'This could be the "results" line') 65 | t.doesNotThrow(write('"resu', 'Another part of the "results" line')) 66 | t.doesNotThrow(write('') , 'Another empty string is still fine') 67 | t.doesNotThrow(write('lts":', 'Final part of "results" line still good')) 68 | t.throws(write(']'), 'First line was not {"results":[') 69 | 70 | feed = new follow.Changes 71 | feed.feed = 'continuous' 72 | t.doesNotThrow(write(''), 'Empty string is fine for a continuous feed') 73 | t.throws(end('{"results":[\n'), 'Continuous stream does not want a header') 74 | 75 | feed = new follow.Changes({'feed':'continuous'}) 76 | t.throws(write('hi\n'), 'Continuous stream wants objects') 77 | 78 | feed = new follow.Changes({'feed':'continuous'}) 79 | t.throws(end('[]\n'), 'Continuous stream wants "real" objects, not Array') 80 | 81 | feed = new follow.Changes({'feed':'continuous'}) 82 | t.throws(write('{"seq":1,"id":"hi","changes":[{"rev":"1-869df2efe56ff5228e613ceb4d561b35"}]},\n'), 83 | 'Continuous stream does not want a comma') 84 | 85 | var types = ['longpoll', 'continuous'] 86 | types.forEach(function(type) { 87 | var bad_writes = [ {}, null, ['a string (array)'], {'an':'object'}] 88 | bad_writes.forEach(function(obj) { 89 | feed = new follow.Changes 90 | feed.feed = type 91 | 92 | t.throws(write(obj), 'Throw for bad write to '+type+': ' + util.inspect(obj)) 93 | }) 94 | 95 | feed = new follow.Changes 96 | feed.feed = type 97 | 98 | var valid = (type == 'longpoll') 99 | ? '{"results":[\n{}\n],\n"last_seq":1}' 100 | : '{"seq":1,"id":"doc"}' 101 | 102 | t.throws(buf(valid, 'but_invalid_encoding'), 'Throw for buffer with bad encoding') 103 | }) 104 | 105 | t.end() 106 | 107 | function buf(data, encoding) { 108 | return write(new Buffer(data), encoding) 109 | } 110 | 111 | function write(data, encoding) { 112 | if(data === undefined) 113 | return feed.write('blah') 114 | return function() { feed.write(data, encoding) } 115 | } 116 | 117 | function end(data, encoding) { 118 | return function() { feed.end(data, encoding) } 119 | } 120 | }) 121 | 122 | test('Longpoll feed', function(t) { 123 | for(var i = 0; i < 2; i++) { 124 | var feed = new follow.Changes({'feed':'longpoll'}) 125 | 126 | var data = [] 127 | feed.on('data', function(d) { data.push(d) }) 128 | 129 | function encode(data) { return (i == 0) ? data : new Buffer(data) } 130 | function write(data) { return function() { feed.write(encode(data)) } } 131 | function end(data) { return function() { feed.end(encode(data)) } } 132 | 133 | t.doesNotThrow(write('{"results":[') , 'Longpoll header') 134 | t.doesNotThrow(write('{}') , 'Empty object') 135 | t.doesNotThrow(write(',{"foo":"bar"},') , 'Comma prefix and suffix') 136 | t.doesNotThrow(write('{"two":"bar"},') , 'Comma suffix') 137 | t.doesNotThrow(write('{"three":3},{"four":4}'), 'Two objects on one line') 138 | t.doesNotThrow(end('],\n"last_seq":3}\n') , 'Longpoll footer') 139 | 140 | t.equal(data.length, 5, 'Five data events fired') 141 | t.equal(data[0], '{}', 'First object emitted') 142 | t.equal(data[1], '{"foo":"bar"}', 'Second object emitted') 143 | t.equal(data[2], '{"two":"bar"}', 'Third object emitted') 144 | t.equal(data[3], '{"three":3}', 'Fourth object emitted') 145 | t.equal(data[4], '{"four":4}', 'Fifth object emitted') 146 | } 147 | 148 | t.end() 149 | }) 150 | 151 | test('Longpoll pause', function(t) { 152 | var feed = new follow.Changes({'feed':'longpoll'}) 153 | , all = {'results':[{'change':1}, {'second':'change'},{'change':'#3'}], 'last_seq':3} 154 | , start = new Date 155 | 156 | var events = [] 157 | 158 | feed.on('data', function(change) { 159 | change = JSON.parse(change) 160 | change.elapsed = new Date - start 161 | events.push(change) 162 | }) 163 | 164 | feed.once('data', function(data) { 165 | t.equal(data, '{"change":1}', 'First data event was the first change') 166 | feed.pause() 167 | setTimeout(function() { feed.resume() }, 100) 168 | }) 169 | 170 | feed.on('end', function() { 171 | t.equal(feed.readable, false, 'Feed is no longer readable') 172 | events.push('END') 173 | }) 174 | 175 | setTimeout(check_events, 150) 176 | feed.end(JSON.stringify(all)) 177 | 178 | function check_events() { 179 | t.equal(events.length, 3+1, 'Three data events, plus the end event') 180 | 181 | t.ok(events[0].elapsed < 10, 'Immediate emit first data event') 182 | t.ok(events[1].elapsed >= 100 && events[1].elapsed < 125, 'About 100ms delay until the second event') 183 | t.ok(events[2].elapsed - events[1].elapsed < 10, 'Immediate emit of subsequent event after resume') 184 | t.equal(events[3], 'END', 'End event was fired') 185 | 186 | t.end() 187 | } 188 | }) 189 | 190 | test('Continuous feed', function(t) { 191 | for(var i = 0; i < 2; i++) { 192 | var feed = new follow.Changes({'feed':'continuous'}) 193 | 194 | var data = [] 195 | feed.on('data', function(d) { data.push(d) }) 196 | feed.on('end', function() { data.push('END') }) 197 | 198 | var beats = 0 199 | feed.on('heartbeat', function() { beats += 1 }) 200 | 201 | function encode(data) { return (i == 0) ? data : new Buffer(data) } 202 | function write(data) { return function() { feed.write(encode(data)) } } 203 | function end(data) { return function() { feed.end(encode(data)) } } 204 | 205 | // This also tests whether the feed is compacting or tightening up the JSON. 206 | t.doesNotThrow(write('{ }\n') , 'Empty object') 207 | t.doesNotThrow(write('\n') , 'Heartbeat') 208 | t.doesNotThrow(write('{ "foo" : "bar" }\n') , 'One object') 209 | t.doesNotThrow(write('{"three":3}\n{ "four": 4}\n'), 'Two objects sent in one chunk') 210 | t.doesNotThrow(write('') , 'Empty string') 211 | t.doesNotThrow(write('\n') , 'Another heartbeat') 212 | t.doesNotThrow(write('') , 'Another empty string') 213 | t.doesNotThrow(write('{ "end" ') , 'Partial object 1/4') 214 | t.doesNotThrow(write(':') , 'Partial object 2/4') 215 | t.doesNotThrow(write('tru') , 'Partial object 3/4') 216 | t.doesNotThrow(end('e}\n') , 'Partial object 4/4') 217 | 218 | t.equal(data.length, 6, 'Five objects emitted, plus the end event') 219 | t.equal(beats, 2, 'Two heartbeats emitted') 220 | 221 | t.equal(data[0], '{}', 'First object emitted') 222 | t.equal(data[1], '{"foo":"bar"}', 'Second object emitted') 223 | t.equal(data[2], '{"three":3}', 'Third object emitted') 224 | t.equal(data[3], '{"four":4}', 'Fourth object emitted') 225 | t.equal(data[4], '{"end":true}', 'Fifth object emitted') 226 | t.equal(data[5], 'END', 'End event fired') 227 | } 228 | 229 | t.end() 230 | }) 231 | 232 | test('Continuous pause', function(t) { 233 | var feed = new follow.Changes({'feed':'continuous'}) 234 | , all = [{'change':1}, {'second':'change'},{'#3':'change'}] 235 | , start = new Date 236 | 237 | var events = [] 238 | 239 | feed.on('end', function() { 240 | t.equal(feed.readable, false, 'Feed is not readable after "end" event') 241 | events.push('END') 242 | }) 243 | 244 | feed.on('data', function(change) { 245 | change = JSON.parse(change) 246 | change.elapsed = new Date - start 247 | events.push(change) 248 | }) 249 | 250 | feed.once('data', function(data) { 251 | t.equal(data, '{"change":1}', 'First data event was the first change') 252 | t.equal(feed.readable, true, 'Feed is readable after first data event') 253 | feed.pause() 254 | t.equal(feed.readable, true, 'Feed is readable after pause()') 255 | 256 | setTimeout(unpause, 100) 257 | function unpause() { 258 | t.equal(feed.readable, true, 'Feed is readable just before resume()') 259 | feed.resume() 260 | } 261 | }) 262 | 263 | setTimeout(check_events, 150) 264 | all.forEach(function(obj) { 265 | feed.write(JSON.stringify(obj)) 266 | feed.write("\r\n") 267 | }) 268 | feed.end() 269 | 270 | function check_events() { 271 | t.equal(events.length, 3+1, 'Three data events, plus the end event') 272 | 273 | t.ok(events[0].elapsed < 10, 'Immediate emit first data event') 274 | t.ok(events[1].elapsed >= 100 && events[1].elapsed < 125, 'About 100ms delay until the second event') 275 | t.ok(events[2].elapsed - events[1].elapsed < 10, 'Immediate emit of subsequent event after resume') 276 | t.equal(events[3], 'END', 'End event was fired') 277 | 278 | t.end() 279 | } 280 | }) 281 | 282 | test('Paused while heartbeats are arriving', function(t) { 283 | var feed = new follow.Changes({'feed':'continuous'}) 284 | , start = new Date 285 | 286 | var events = [] 287 | 288 | feed.on('data', function(change) { 289 | change = JSON.parse(change) 290 | change.elapsed = new Date - start 291 | events.push(change) 292 | }) 293 | 294 | feed.on('heartbeat', function(hb) { 295 | var hb = {'heartbeat':true, 'elapsed':(new Date) - start} 296 | events.push(hb) 297 | }) 298 | 299 | feed.once('data', function(data) { 300 | t.equal(data, '{"change":1}', 'First data event was the first change') 301 | feed.pause() 302 | t.equal(feed.readable, true, 'Feed is readable after pause()') 303 | 304 | setTimeout(unpause, 350) 305 | function unpause() { 306 | t.equal(feed.readable, true, 'Feed is readable just before resume()') 307 | feed.resume() 308 | } 309 | }) 310 | 311 | // Simulate a change, then a couple heartbeats every 100ms, then more changes. 312 | var writes = [] 313 | function write(data) { 314 | var result = feed.write(data) 315 | writes.push(result) 316 | } 317 | 318 | setTimeout(function() { write('{"change":1}\n') }, 0) 319 | setTimeout(function() { write('\n') }, 100) 320 | setTimeout(function() { write('\n') }, 200) 321 | setTimeout(function() { write('\n') }, 300) 322 | setTimeout(function() { write('\n') }, 400) 323 | setTimeout(function() { write('{"change":2}\n') }, 415) 324 | setTimeout(function() { write('{"change":3}\n') }, 430) 325 | 326 | setTimeout(check_events, 500) 327 | function check_events() { 328 | t.ok(events[0].change , 'First event was data (a change)') 329 | t.ok(events[1].heartbeat, 'Second event was a heartbeat') 330 | t.ok(events[2].heartbeat, 'Third event was a heartbeat') 331 | t.ok(events[3].heartbeat, 'Fourth event was a heartbeat') 332 | t.ok(events[4].heartbeat, 'Fifth event was a heartbeat') 333 | t.ok(events[5].change , 'Sixth event was data') 334 | t.ok(events[6].change , 'Seventh event was data') 335 | 336 | t.ok(events[0].elapsed < 10, 'Immediate emit first data event') 337 | t.ok(events[1].elapsed > 350, 'First heartbeat comes after the pause') 338 | t.ok(events[2].elapsed > 350, 'Second heartbeat comes after the pause') 339 | t.ok(events[3].elapsed > 350, 'Third heartbeat comes after the pause') 340 | t.ok(events[4].elapsed > 350, 'Fourth heartbeat comes after the pause') 341 | 342 | t.ok(events[3].elapsed < 360, 'Third heartbeat came right after the resume') 343 | t.ok(events[4].elapsed >= 400, 'Fourth heartbeat came at the normal time') 344 | 345 | t.equal(writes[0], true , 'First write flushed') 346 | t.equal(writes[1], false, 'Second write (first heartbeat) did not flush because the feed was paused') 347 | t.equal(writes[2], false, 'Third write did not flush due to pause') 348 | t.equal(writes[3], false, 'Fourth write did not flush due to pause') 349 | t.equal(writes[4], true , 'Fifth write (fourth heartbeat) flushed due to resume()') 350 | t.equal(writes[5], true , 'Sixth write flushed normally') 351 | t.equal(writes[6], true , 'Seventh write flushed normally') 352 | 353 | feed.end() 354 | t.end() 355 | } 356 | }) 357 | 358 | test('Feeds from couch', function(t) { 359 | t.ok(couch.rtt(), 'RTT to couch is known') 360 | 361 | var did = 0 362 | function done() { 363 | did += 1 364 | if(did == 2) 365 | t.end() 366 | } 367 | 368 | var types = [ 'longpoll', 'continuous' ] 369 | types.forEach(function(type) { 370 | var feed = new follow.Changes({'feed':type}) 371 | setTimeout(check_changes, couch.rtt() * 2) 372 | 373 | var events = [] 374 | feed.on('data', function(data) { events.push(JSON.parse(data)) }) 375 | feed.on('end', function() { events.push('END') }) 376 | 377 | var uri = couch.DB + '/_changes?feed=' + type 378 | var req = request({'uri':uri}) 379 | 380 | // Compatibility with the old onResponse option. 381 | req.on('response', function(res) { on_response(null, res, res.body) }) 382 | 383 | // Disconnect the continuous feed after a while. 384 | if(type == 'continuous') 385 | setTimeout(function() { feed.destroy() }, couch.rtt() * 1) 386 | 387 | function on_response(er, res, body) { 388 | t.false(er, 'No problem fetching '+type+' feed: ' + uri) 389 | t.type(body, 'undefined', 'No data in '+type+' callback. This is an onResponse callback') 390 | t.type(res.body, 'undefined', 'No response body in '+type+' callback. This is an onResponse callback') 391 | t.ok(req.response, 'The request object has its '+type+' response by now') 392 | 393 | req.pipe(feed) 394 | 395 | t.equal(feed.statusCode, 200, 'Upon piping from request, the statusCode is set') 396 | t.ok('content-type' in feed.headers, 'Upon piping from request, feed has headers set') 397 | } 398 | 399 | function check_changes() { 400 | var expected_count = 3 401 | if(type == 'longpoll') 402 | expected_count += 1 // For the "end" event 403 | 404 | t.equal(events.length, expected_count, 'Change event count for ' + type) 405 | 406 | t.equal(events[0].seq, 1, 'First '+type+' update sequence id') 407 | t.equal(events[1].seq, 2, 'Second '+type+' update sequence id') 408 | t.equal(events[2].seq, 3, 'Third '+type+' update sequence id') 409 | 410 | t.equal(good_id(events[0]), true, 'First '+type+' update is a good doc id: ' + events[0].id) 411 | t.equal(good_id(events[1]), true, 'Second '+type+' update is a good doc id: ' + events[1].id) 412 | t.equal(good_id(events[2]), true, 'Third '+type+' update is a good doc id: ' + events[2].id) 413 | 414 | if(type == 'longpoll') 415 | t.equal(events[3], 'END', 'End event fired for '+type) 416 | else 417 | t.type(events[3], 'undefined', 'No end event for a destroyed continuous feed') 418 | 419 | done() 420 | } 421 | 422 | var good_ids = {'doc_first':true, 'doc_second':true, 'doc_third':true} 423 | function good_id(event) { 424 | var is_good = good_ids[event.id] 425 | delete good_ids[event.id] 426 | return is_good 427 | } 428 | }) 429 | }) 430 | 431 | test('Pausing and destroying a feed mid-stream', function(t) { 432 | t.ok(couch.rtt(), 'RTT to couch is known') 433 | var IMMEDIATE = 20 434 | , FIRST_PAUSE = couch.rtt() * 8 435 | , SECOND_PAUSE = couch.rtt() * 12 436 | 437 | // To be really, really sure that backpressure goes all the way to couch, create more 438 | // documents than could possibly be buffered. Linux and OSX seem to have a 16k MTU for 439 | // the local interface, so a few hundred kb worth of document data should cover it. 440 | couch.make_data(512 * 1024, check) 441 | 442 | var types = ['longpoll', 'continuous'] 443 | function check(bulk_docs_count) { 444 | var type = types.shift() 445 | if(!type) 446 | return t.end() 447 | 448 | var feed = new follow.Changes 449 | feed.feed = type 450 | 451 | var destroys = 0 452 | function destroy() { 453 | destroys += 1 454 | feed.destroy() 455 | 456 | // Give one more RTT time for everything to wind down before checking how it all went. 457 | if(destroys == 1) 458 | setTimeout(check_events, couch.rtt()) 459 | } 460 | 461 | var events = {'feed':[], 'http':[], 'request':[]} 462 | , firsts = {'feed':null, 'http':null, 'request':null} 463 | 464 | function ev(type, value) { 465 | var now = new Date 466 | firsts[type] = firsts[type] || now 467 | events[type].push({'elapsed':now - firsts[type], 'at':now, 'val':value, 'before_destroy':(destroys == 0)}) 468 | } 469 | 470 | feed.on('heartbeat', function() { ev('feed', 'heartbeat') }) 471 | feed.on('error', function(er) { ev('feed', er) }) 472 | feed.on('close', function() { ev('feed', 'close') }) 473 | feed.on('data', function(data) { ev('feed', data) }) 474 | feed.on('end', function() { ev('feed', 'end') }) 475 | 476 | var data_count = 0 477 | feed.on('data', function() { 478 | data_count += 1 479 | if(data_count == 4) { 480 | feed.pause() 481 | setTimeout(function() { feed.resume() }, FIRST_PAUSE) 482 | } 483 | 484 | if(data_count == 7) { 485 | feed.pause() 486 | setTimeout(function() { feed.resume() }, SECOND_PAUSE - FIRST_PAUSE) 487 | } 488 | 489 | if(data_count >= 10) 490 | destroy() 491 | }) 492 | 493 | var uri = couch.DB + '/_changes?include_docs=true&feed=' + type 494 | if(type == 'continuous') 495 | uri += '&heartbeat=' + Math.floor(couch.rtt()) 496 | 497 | var req = request({'uri':uri}) 498 | req.on('response', function(res) { feed_response(null, res, res.body) }) 499 | req.on('error', function(er) { ev('request', er) }) 500 | req.on('close', function() { ev('request', 'close') }) 501 | req.on('data', function(d) { ev('request', d) }) 502 | req.on('end', function() { ev('request', 'end') }) 503 | req.pipe(feed) 504 | 505 | function feed_response(er, res) { 506 | if(er) throw er 507 | 508 | res.on('error', function(er) { ev('http', er) }) 509 | res.on('close', function() { ev('http', 'close') }) 510 | res.on('data', function(d) { ev('http', d) }) 511 | res.on('end', function() { ev('http', 'end') }) 512 | 513 | t.equal(events.feed.length, 0, 'No feed events yet: ' + type) 514 | t.equal(events.http.length, 0, 'No http events yet: ' + type) 515 | t.equal(events.request.length, 0, 'No request events yet: ' + type) 516 | } 517 | 518 | function check_events() { 519 | t.equal(destroys, 1, 'Only necessary to call destroy once: ' + type) 520 | t.equal(events.feed.length, 10, 'Ten '+type+' data events fired') 521 | if(events.feed.length != 10) 522 | events.feed.forEach(function(e, i) { 523 | console.error((i+1) + ') ' + util.inspect(e)) 524 | }) 525 | 526 | events.feed.forEach(function(event, i) { 527 | var label = type + ' event #' + (i+1) + ' at ' + event.elapsed + ' ms' 528 | 529 | t.type(event.val, 'string', label+' was a data string') 530 | t.equal(event.before_destroy, true, label+' fired before the destroy') 531 | 532 | var change = null 533 | t.doesNotThrow(function() { change = JSON.parse(event.val) }, label+' was JSON: ' + type) 534 | t.ok(change && change.seq > 0 && change.id, label+' was change data: ' + type) 535 | 536 | // The first batch of events should have fired quickly (IMMEDIATE), then silence. Then another batch 537 | // of events at the FIRST_PAUSE mark. Then more silence. Then a final batch at the SECOND_PAUSE mark. 538 | if(i < 4) 539 | t.ok(event.elapsed < IMMEDIATE, label+' was immediate (within '+IMMEDIATE+' ms)') 540 | else if(i < 7) 541 | t.ok(is_almost(event.elapsed, FIRST_PAUSE), label+' was after the first pause (about '+FIRST_PAUSE+' ms)') 542 | else 543 | t.ok(is_almost(event.elapsed, SECOND_PAUSE), label+' was after the second pause (about '+SECOND_PAUSE+' ms)') 544 | }) 545 | 546 | if(type == 'continuous') { 547 | t.ok(events.http.length >= 6, 'Should have at least seven '+type+' HTTP events') 548 | t.ok(events.request.length >= 6, 'Should have at least seven '+type+' request events') 549 | 550 | t.ok(events.http.length < 200, type+' HTTP events ('+events.http.length+') stop before 200') 551 | t.ok(events.request.length < 200, type+' request events ('+events.request.length+') stop before 200') 552 | 553 | var frac = events.http.length / bulk_docs_count 554 | t.ok(frac < 0.10, 'Percent of http events received ('+frac.toFixed(1)+'%) is less than 10% of the data') 555 | 556 | frac = events.request.length / bulk_docs_count 557 | t.ok(frac < 0.10, type+' request events received ('+frac.toFixed(1)+'%) is less than 10% of the data') 558 | } 559 | 560 | return check(bulk_docs_count) 561 | } 562 | } 563 | }) 564 | 565 | // 566 | // Utilities 567 | // 568 | 569 | function is_almost(actual, expected) { 570 | var tolerance = 0.10 // 10% 571 | , diff = Math.abs(actual - expected) 572 | , fraction = diff / expected 573 | return fraction < tolerance 574 | } 575 | --------------------------------------------------------------------------------