├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── build.js ├── dev-server.js ├── es3ify.js └── test-browser.js ├── client └── index.js ├── dist ├── pouchdb.worker-pouch.js └── pouchdb.worker-pouch.min.js ├── lib ├── client │ ├── core.js │ ├── create-worker.js │ ├── index.js │ ├── is-supported-browser.js │ └── utils.js ├── index.js ├── shared │ ├── errors.js │ ├── pouchdb-clone.js │ ├── utils.js │ └── uuid.js └── worker │ ├── core.js │ ├── index.js │ ├── pouchdb-idb-only.js │ ├── safe-eval.js │ └── utils.js ├── package-lock.json ├── package.json ├── test ├── bind-polyfill.js ├── custom-api │ ├── service-worker-test.js │ ├── service-worker.js │ ├── test.js │ └── worker.js ├── deps │ └── bigimage.js ├── index-suite1.html ├── index-suite2.html ├── index-suite3.html ├── index-suite4.html ├── index-suite5.html ├── index-suite6.html ├── index-suite7.html ├── index-suite8.html ├── index.html ├── pouchdb │ ├── integration │ │ ├── test.aa.setup.js │ │ ├── test.all_docs.js │ │ ├── test.attachments.js │ │ ├── test.basics.js │ │ ├── test.bulk_docs.js │ │ ├── test.bulk_get.js │ │ ├── test.changes.js │ │ ├── test.compaction.js │ │ ├── test.conflicts.js │ │ ├── test.constructor.js │ │ ├── test.defaults.js │ │ ├── test.design_docs.js │ │ ├── test.events.js │ │ ├── test.get.js │ │ ├── test.http.js │ │ ├── test.issue1175.js │ │ ├── test.issue221.js │ │ ├── test.issue3179.js │ │ ├── test.issue3646.js │ │ ├── test.issue915.js │ │ ├── test.local_docs.js │ │ ├── test.migration.js │ │ ├── test.replication.js │ │ ├── test.replication_events.js │ │ ├── test.reserved.js │ │ ├── test.retry.js │ │ ├── test.revs_diff.js │ │ ├── test.slash_id.js │ │ ├── test.sync.js │ │ ├── test.sync_events.js │ │ ├── test.taskqueue.js │ │ ├── test.uuids.js │ │ └── utils.js │ └── mapreduce │ │ ├── test.mapreduce.js │ │ ├── test.persisted.js │ │ └── test.views.js ├── test.js ├── test.supported-browser.js └── webrunner.js └── worker └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *~ 4 | coverage 5 | test/test-bundle.js 6 | npm-debug.log 7 | testdb* 8 | test/custom-api/worker-bundle.js 9 | lib/workerified/index.js 10 | test/sw-test-bundle.js 11 | test/sw.js 12 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | lib/workerified/index.js 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "newcap": true, 6 | "noarg": true, 7 | "sub": true, 8 | "undef": true, 9 | "unused": true, 10 | "eqnull": true, 11 | "browser": true, 12 | "node": true, 13 | "strict": true, 14 | "globalstrict": true, 15 | "globals": { "eio": true}, 16 | "white": true, 17 | "indent": 2, 18 | "maxlen": 100, 19 | "predef": [ 20 | "chrome", 21 | "eio", 22 | "process", 23 | "global", 24 | "require", 25 | "console", 26 | "describe", 27 | "beforeEach", 28 | "afterEach", 29 | "before", 30 | "after", 31 | "it", 32 | "emit" 33 | ] 34 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | node_modules 3 | .DS_Store 4 | *~ 5 | coverage 6 | npm-debug.log 7 | vendor/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | services: 4 | - couchdb 5 | 6 | node_js: 7 | - "5" 8 | 9 | sudo: false 10 | 11 | addons: 12 | firefox: "53.0" 13 | apt: 14 | packages: 15 | - oracle-java8-set-default 16 | 17 | script: npm run $COMMAND 18 | 19 | before_script: 20 | - firefox --version 21 | - java -version 22 | - "export DISPLAY=:99.0" 23 | - "sh -e /etc/init.d/xvfb start" 24 | 25 | - "npm install add-cors-to-couchdb" 26 | - "./node_modules/.bin/add-cors-to-couchdb" 27 | 28 | env: 29 | matrix: 30 | # break into many small tests so travis doesn't complain 31 | - CLIENT=selenium:firefox SUITE=1 COMMAND=test 32 | - CLIENT=selenium:firefox SUITE=2 COMMAND=test 33 | - CLIENT=selenium:firefox SUITE=3 COMMAND=test 34 | - CLIENT=selenium:firefox SUITE=4 COMMAND=test 35 | - CLIENT=selenium:firefox SUITE=5 COMMAND=test 36 | - CLIENT=selenium:firefox SUITE=6 COMMAND=test 37 | - CLIENT=selenium:firefox SUITE=7 COMMAND=test 38 | - CLIENT=selenium:firefox SUITE=8 COMMAND=test 39 | - COMMAND=test-custom 40 | 41 | branches: 42 | only: 43 | - master 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - 2.1.0 2 | - ([#43](https://github.com/pouchdb-community/worker-pouch/issues/43)): Fix db.type() deprecation warning 3 | - ([#34](https://github.com/pouchdb-community/worker-pouch/issues/34)): id() returns the db name instead of a uuid 4 | - ([#24](https://github.com/pouchdb-community/worker-pouch/issues/24)): Pass `storage` option through to IDB 5 | - ([#40](https://github.com/pouchdb-community/worker-pouch/pull/40)): Use `_interopDefault` on `require('pouchdb-promise')` 6 | - 2.0.0 7 | - BREAKING: support for PouchDB 6.0.0. For PouchDB <6, use worker-pouch <2. 8 | - 1.0.0 9 | - Initial release 10 | -------------------------------------------------------------------------------- /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 | worker-pouch [](https://travis-ci.org/pouchdb-community/worker-pouch) 2 | ===== 3 | 4 | ```js 5 | // This pouch is powered by Workers! 6 | var db = new PouchDB('mydb', {adapter: 'worker'}); 7 | ``` 8 | 9 | Adapter plugin to use PouchDB over [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Worker) and [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). Transparently proxies all PouchDB API requests to the worker, so that the most expensive database operations are run in a separate thread. 10 | 11 | Basically, worker-pouch allows you use the PouchDB API like you normally would, but your UI will suffer fewer hiccups, because any blocking operations (such as IndexedDB or checksumming) are run inside of the worker. 12 | 13 | The worker-pouch adapter passes [the full PouchDB test suite](https://travis-ci.org/pouchdb-community/worker-pouch). It requires PouchDB 5.0.0+. 14 | 15 | **Topics** 16 | 17 | * [Install](#install) 18 | * [Usage](#usage) 19 | * [Easy Mode](#easy-mode) 20 | * [Custom Mode](#custom-mode) 21 | * [Performance benefits of worker-pouch](#performance-benefits-of-worker-pouch) 22 | * [Fallback for unsupported browsers](#fallback-for-unsupported-browsers) 23 | * [Debugging](#debugging) 24 | * [FAQs](#faqs) 25 | * [Changelog](#changelog) 26 | 27 | 28 | Install 29 | --- 30 | 31 | $ npm install worker-pouch 32 | 33 | Usage 34 | --- 35 | 36 | This plugin has two modes: 37 | * [Easy Mode](#easy-mode), for Web Workers, supporting Chrome and Firefox, with a fallback for other browsers, and 38 | * [Custom Mode](#custom-mode), potentially supporting more browsers, for Service Workers and allowing you to use your own Web Worker. 39 | 40 | In Easy Mode, you don't need to set up the worker yourself, because the script is loaded in a [Blob URL](https://developer.mozilla.org/en-US/docs/Web/API/Blob). Whereas in Custom Mode, you must manage the Web Worker or Service Worker yourself. 41 | 42 | ### Easy Mode 43 | 44 | You can do Easy Mode either with prebuilt JavaScript or via Browserify/Webpack. 45 | 46 | #### Setup via prebuilt JavaScript 47 | 48 | The client JS file is available at `node_modules/worker-pouch/dist/pouchdb.worker-pouch.js`. Or you can just download it from Github above (in which case, it will be available as `window.workerPouch`). 49 | 50 | Then include it in your HTML, after PouchDB: 51 | 52 | ```html 53 | 54 | 55 | ``` 56 | 57 | Then you can create a worker-powered PouchDB using: 58 | 59 | ```js 60 | var db = new PouchDB('mydb', {adapter: 'worker'}); 61 | ``` 62 | 63 | #### Setup via Browserify/Webpack/etc. 64 | 65 | The same rules apply, but you have to notify PouchDB of the new adapter: 66 | 67 | ```js 68 | var PouchDB = require('pouchdb'); 69 | PouchDB.adapter('worker', require('worker-pouch')); 70 | ``` 71 | 72 | #### Detecting browser support 73 | 74 | Unfortunately, creating workers via Blob URLs is not supported in all browsers. In particular, IE, Edge, Safari, and iOS are not supported. Luckily, Firefox and Chrome are the browsers that [benefit the most from web workers](http://nolanlawson.com/2015/09/29/indexeddb-websql-localstorage-what-blocks-the-dom/). There is also an API to [detect browser support](#fallback-for-unsupported-browsers), which you must use if you would like to support browsers other than Firefox and Chrome. 75 | 76 | ### Custom Mode 77 | 78 | In this mode, you manage the Web Worker yourself, and you register the two endpoints so that worker-pouch can communicate with the "backend" and "frontend." 79 | 80 | Since this doesn't require Blob URLs, and because you can use custom PouchDB objects, you can potentially support more browsers this way. It's much more flexible. 81 | 82 | This mode only supports bundling via Browserify/Webpack/etc. There is no prebuilt option. 83 | 84 | To use, you'll need this code on the client side: 85 | 86 | ```js 87 | // client-side code 88 | var PouchDB = require('pouchdb'); 89 | PouchDB.adapter('worker', require('worker-pouch/client')); 90 | 91 | var worker = new Worker('worker.js'); 92 | 93 | var db = new PouchDB('mydb', { 94 | adapter: 'worker', 95 | worker: worker 96 | }); 97 | ``` 98 | 99 | Note that you create the `PouchDB` object passing in both `adapter: 'worker'` 100 | and `worker`, which points to your `Worker` object. 101 | 102 | Then you include this code on the worker side: 103 | 104 | ```js 105 | // worker-side code 106 | var registerWorkerPouch = require('worker-pouch/worker'); 107 | var PouchDB = require('pouchdb'); 108 | 109 | // attach to global `self` object 110 | registerWorkerPouch(self, PouchDB); 111 | ``` 112 | 113 | If you would like to customize how `PouchDB` is created inside of the worker, then you can also pass in a custom PouchDB factory function, which is a function that takes an options object (e.g. `{name: 'mydb', auto_compaction: true}`) 114 | and returns a `PouchDB` object. 115 | 116 | This is useful in cases where PouchDB's IndexedDB adapter doesn't work inside of a worker ([such as Safari](https://bugs.webkit.org/show_bug.cgi?id=149953)), so for instance you can have the `pouchCreator` function 117 | return an in-memory `PouchDB` object. 118 | 119 | Here's an example: 120 | 121 | ```js 122 | var PouchDB = require('pouchdb'); 123 | require('pouchdb/extras/memory'); 124 | function pouchCreator(opts) { 125 | opts.adapter = 'memory'; // force in-memory mode 126 | return new PouchDB(opts); 127 | } 128 | 129 | var registerWorkerPouch = require('worker-pouch/worker'); 130 | registerWorkerPouch(self, pouchCreator); 131 | 132 | ``` 133 | 134 | The PouchDB worker code will listen for messages from the client side, but should ignore any non-worker-pouch messages, so you are free to still use `worker.postMessage()` as desired. 135 | 136 | #### Service Workers 137 | 138 | Communicating with a Service Worker is the same as with a Web Worker. 139 | However, you have to wait for the Service Worker to install and start controlling the page. Here's an example: 140 | 141 | ```js 142 | navigator.serviceWorker.register('sw.js', { 143 | scope: './' 144 | }).then(function () { 145 | if (navigator.serviceWorker.controller) { 146 | // already active and controlling this page 147 | return navigator.serviceWorker; 148 | } 149 | // wait for a new service worker to control this page 150 | return new Promise(function (resolve) { 151 | function onControllerChange() { 152 | navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange); 153 | resolve(navigator.serviceWorker); 154 | } 155 | navigator.serviceWorker.addEventListener('controllerchange', onControllerChange); 156 | }); 157 | }).then(function (serviceWorker) { // the worker is ready 158 | db = new PouchDB('testdb', { 159 | adapter: 'worker', 160 | worker: function() { 161 | return serviceWorker; 162 | } 163 | }); 164 | return db; 165 | }).catch(console.log.bind(console)); 166 | }); 167 | ``` 168 | 169 | Then inside your Service Worker: 170 | 171 | ```js 172 | // worker-side code 173 | var registerWorkerPouch = require('worker-pouch/worker'); 174 | var PouchDB = require('pouchdb'); 175 | 176 | // attach to global `self` object 177 | registerWorkerPouch(self, PouchDB); 178 | 179 | self.addEventListener('activate', function(event) { 180 | event.waitUntil(self.clients.claim()); // activate right now 181 | }); 182 | ``` 183 | 184 | Performance benefits of worker-pouch 185 | ---- 186 | 187 | These numbers were recorded using [this site](http://nolanlawson.github.io/database-comparison-worker-pouch/). The test involved inserting 10000 PouchDB documents, and was run on a 2013 MacBook Air. Browser data was deleted between each test. 188 | 189 | | | Time (ms) | Blocked the DOM? | 190 | | ----- | ------ | ------ | 191 | | *Chrome 48* | | | 192 | | `put()` - normal | 50070 | No | 193 | | `put()` - worker | 56993 | No | 194 | | `bulkDocs()` - normal | 2740 | Yes | 195 | | `bulkDocs()` - worker| 3454 | No | 196 | | *Firefox 43* | | | 197 | | `put()` - normal | 39595 | No | 198 | | `put()` - worker | 41425 | No | 199 | | `bulkDocs()` - normal | 1027 | Yes | 200 | | `bulkDocs()` - worker| 1130 | No | 201 | 202 | Basic takeaway: `put()`s avoid DOM-blocking (due to using many smaller transactions), but are much slower than `bulkDocs()`. With worker-pouch, though, you can get nearly all the speed benefit of `bulkDocs()` without blocking the DOM. 203 | 204 | (Note that by "blocked the DOM," I mean froze the animated GIF for a significant amount of time - at least a half-second. A single dropped frame was not penalized. Try the test yourself, and you'll see the difference is pretty stark.) 205 | 206 | Fallback for unsupported browsers 207 | ---- 208 | 209 | In Easy Mode, this plugin doesn't support all browsers. So it provides a special API to dianogose whether or not the current browser supports worker-pouch. Here's how you can use it: 210 | 211 | ```js 212 | var workerPouch = require('worker-pouch'); 213 | 214 | workerPouch.isSupportedBrowser().then(function (supported) { 215 | var db; 216 | if (supported) { 217 | db = new PouchDB('mydb', {adapter: 'worker'}); 218 | } else { // fall back to a normal PouchDB 219 | db = new PouchDB('mydb'); 220 | } 221 | }).catch(console.log.bind(console)); // shouldn't throw an error 222 | ``` 223 | 224 | The `isSupportedBrowser()` API returns a Promise for a boolean, which will be `true` if the browser is supported and `false` otherwise. 225 | 226 | If you are using this method to return the PouchDB object *itself* from a Promise, be sure to wrap it in an object, to avoid "circular promise" errors: 227 | 228 | ```js 229 | var workerPouch = require('worker-pouch'); 230 | 231 | workerPouch.isSupportedBrowser().then(function (supported) { 232 | if (supported) { 233 | return {db: new PouchDB('mydb', {adapter: 'worker'})}; 234 | } else { // fall back to a normal PouchDB 235 | return {db: new PouchDB('mydb')}; 236 | } 237 | }).then(function (dbWrapper) { 238 | var db = dbWrapper.db; // now I have a PouchDB 239 | }).catch(console.log.bind(console)); // shouldn't throw an error 240 | ``` 241 | 242 | Debugging 243 | ----- 244 | 245 | worker-pouch uses [debug](https://github.com/visionmedia/debug) for logging. So in the browser, you can enable debugging by using PouchDB's logger: 246 | 247 | ```js 248 | PouchDB.debug.enable('pouchdb:worker:*'); 249 | ``` 250 | 251 | FAQs 252 | --- 253 | 254 | #### Wait, doesn't PouchDB already work in a Web Worker or Service Worker? 255 | 256 | Yes, you can use pure PouchDB inside of a Web Worker or Service Worker. But the point of this plugin is to let you use PouchDB from *outside a Web Worker or Service Worker*, and then have it transparently proxy to another PouchDB that is isolated in a Web Worker or Service Worker. 257 | 258 | #### What browsers are supported? 259 | 260 | Only those browsers that 1) Allow service workers or 2) Allow blob URLs for Web Worker scripts and allow IndexedDB inside of a Web Worker. Today, that means Chrome and Firefox. 261 | 262 | #### Can I use it with other plugins? 263 | 264 | Not right now, although map/reduce is supported. 265 | 266 | #### Don't I pay a heavy cost of structured cloning due to worker messages? 267 | 268 | Yes, but apparently this cost is less than that of IndexedDB, because the DOM is significanty less blocked when using worker-pouch. Another thing to keep in mind is that PouchDB's internal document representation in IndexedDB is more complex than the PouchDB documents you insert. So you clone a small PouchDB object to send it to the worker, and then inside the worker it's exploded into a more complex IndexedDB object. IndexedDB itself has to clone as well, but the more complex cloning is done inside the worker. 269 | 270 | #### Does replication occur inside the worker? 271 | 272 | It's a bit subtle. The answer is **yes**, if you do this: 273 | 274 | ```js 275 | var local = new PouchDB('local', {adapter: 'worker'}); 276 | local.replicate.to('http://example.com/db'); 277 | ``` 278 | 279 | However, the answer is **no** if you do: 280 | 281 | ```js 282 | var local = new PouchDB('local', {adapter: 'worker'}); 283 | var remote = new PouchDB('http://example.com/db'); 284 | local.replicate.to(remote); 285 | ``` 286 | 287 | The reason is that when you create a remote PouchDB using `new PouchDB('http://example.com/db')`, then that runs inside the UI thread. However, when you `.replicate.to('http://example.com/db')`, then that string is passed ver-batim to the worker thread, where `worker-pouch` becomes responsible for creating the remote PouchDB. Hence replication will occur inside of the worker thread. 288 | 289 | In general, if you are very concerned about performance implications of what runs inside of the woker vs what runs outside of the worker, you are encouraged to _not_ use `worker-pouch` and to instead just run PouchDB inside a worker and handle message-passing yourself (might I recommend [promise-worker](https://github.com/nolanlawson/promise-worker)?). This is the only way to really ensure that _all_ PouchDB operations are isolated to the worker. 290 | 291 | Changelog 292 | ----- 293 | 294 | - 1.1.0 295 | - Adds the Custom Mode API 296 | - 1.0.0 297 | - Initial release 298 | 299 | Building 300 | ---- 301 | 302 | npm install 303 | npm run build 304 | 305 | Your plugin is now located at `dist/pouchdb.worker-pouch.js` and `dist/pouchdb.worker-pouch.min.js` and is ready for distribution. 306 | 307 | Testing 308 | ---- 309 | 310 | ### In the browser 311 | 312 | Run `npm run dev` and then point your favorite browser to [http://127.0.0.1:8000/test/index.html](http://127.0.0.1:8000/test/index.html). 313 | 314 | The query param `?grep=mysearch` will search for tests matching `mysearch`. 315 | 316 | ### Automated browser tests 317 | 318 | You can run e.g. 319 | 320 | CLIENT=selenium:firefox npm test 321 | CLIENT=selenium:phantomjs npm test 322 | 323 | This will run the tests automatically and the process will exit with a 0 or a 1 when it's done. Firefox uses IndexedDB, and PhantomJS uses WebSQL. 324 | 325 | ### Running the custom-api tests 326 | 327 | Run: 328 | 329 | npm run test-custom 330 | 331 | Or to debug: 332 | 333 | npm run test-custom-local 334 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var browserify = require('browserify'); 6 | var UglifyJS = require("uglify-js"); 7 | var Promise = require('bluebird'); 8 | var fs = require('fs'); 9 | var writeFile = Promise.promisify(fs.writeFile.bind(fs)); 10 | var mkdirp = Promise.promisify(require('mkdirp')); 11 | var derequire = require('derequire'); 12 | 13 | function browserifyIt(entry) { 14 | return new Promise(function (resolve, reject) { 15 | var data = ''; 16 | var b = browserify().plugin('bundle-collapser/plugin'); 17 | 18 | b.add(entry); 19 | var bundle = b.bundle(); 20 | bundle.on('data', function (buf) { 21 | data += buf; 22 | }); 23 | bundle.on('end', function () { 24 | data = derequire(data); 25 | data = UglifyJS.minify(data, { 26 | fromString: true, 27 | mangle: true, 28 | compress: true 29 | }).code; 30 | resolve(data); 31 | }).on('error', reject); 32 | }); 33 | } 34 | return mkdirp('./lib/workerified').then(function () { 35 | return browserifyIt('./lib/worker/index.js'); 36 | }).then(function (data) { 37 | var code = "// this code is automatically generated by bin/build.js\n" + 38 | "module.exports = " + JSON.stringify(data) + ';'; 39 | return writeFile('./lib/workerified/index.js', code, 'utf-8'); 40 | }).catch(function (err) { 41 | console.error(err.stack); 42 | process.exit(1); 43 | }); -------------------------------------------------------------------------------- /bin/dev-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var COUCH_HOST = process.env.COUCH_HOST || 'http://127.0.0.1:5984'; 6 | var HTTP_PORT = 8000; 7 | var CORS_PORT = 2020; 8 | 9 | var cors_proxy = require('corsproxy'); 10 | var watch = require('watch-glob'); 11 | var Promise = require('bluebird'); 12 | var http_proxy = require('pouchdb-http-proxy'); 13 | var http_server = require("http-server"); 14 | var debounce = require('lodash.debounce'); 15 | var fs = require('fs'); 16 | var browserify = require('browserify'); 17 | var filesWritten = false; 18 | var serverStarted = false; 19 | var readyCallback; 20 | 21 | var rebuildPromise = Promise.resolve(); 22 | 23 | function browserifyPromise(src, dest) { 24 | return new Promise(function (resolve, reject) { 25 | browserify(src, {debug: true}).bundle().pipe(fs.createWriteStream(dest)) 26 | .on('finish', resolve) 27 | .on('error', reject); 28 | }); 29 | } 30 | 31 | function rebuildServiceWorker() { 32 | rebuildPromise = rebuildPromise.then(function () { 33 | return browserifyPromise('./test/custom-api/service-worker.js', 34 | './test/sw.js'); 35 | }).then(function () { 36 | console.log('Rebuilt test/sw.js'); 37 | }).catch(console.error); 38 | return rebuildPromise; 39 | } 40 | 41 | function rebuildServiceWorkerTest() { 42 | rebuildPromise = rebuildPromise.then(function () { 43 | return browserifyPromise('./test/custom-api/service-worker-test.js', 44 | './test/sw-test-bundle.js'); 45 | }).then(function () { 46 | console.log('Rebuilt test/sw-test-bundle.js'); 47 | }).catch(console.error); 48 | return rebuildPromise; 49 | } 50 | 51 | function rebuildTestBundle() { 52 | rebuildPromise = rebuildPromise.then(function () { 53 | return browserifyPromise('./test/test.js', 54 | './test/test-bundle.js'); 55 | }).then(function () { 56 | console.log('Rebuilt test/test-bundle.js'); 57 | }).catch(console.error); 58 | return rebuildPromise; 59 | } 60 | 61 | function watchAll() { 62 | watch(['./test/custom-api/service-worker.js'], 63 | debounce(rebuildServiceWorker, 700, {leading: true})); 64 | watch(['./test/custom-api/service-worker-test.js'], 65 | debounce(rebuildServiceWorkerTest, 700, {leading: true})); 66 | watch(['./test/test.js";'], 67 | debounce(rebuildTestBundle, 700, {leading: true})); 68 | } 69 | 70 | Promise.all([ 71 | rebuildTestBundle(), 72 | rebuildServiceWorker(), 73 | rebuildServiceWorkerTest() 74 | ]).then(() => { 75 | console.log('Rebuilt test bundles'); 76 | filesWritten = true; 77 | checkReady(); 78 | }); 79 | 80 | function startServers(callback) { 81 | readyCallback = callback; 82 | 83 | return new Promise(function (resolve, reject) { 84 | http_server.createServer().listen(HTTP_PORT, function (err) { 85 | if (err) { 86 | return reject(err); 87 | } 88 | cors_proxy.options = {target: COUCH_HOST}; 89 | http_proxy.createServer(cors_proxy).listen(CORS_PORT, function (err) { 90 | if (err) { 91 | return reject(err); 92 | } 93 | resolve(); 94 | }); 95 | }); 96 | }).then(function () { 97 | console.log('Tests: http://127.0.0.1:' + HTTP_PORT + '/test/index.html'); 98 | serverStarted = true; 99 | checkReady(); 100 | }).catch(function (err) { 101 | if (err) { 102 | console.log(err); 103 | process.exit(1); 104 | } 105 | }); 106 | } 107 | 108 | function checkReady() { 109 | if (filesWritten && serverStarted && readyCallback) { 110 | readyCallback(); 111 | } 112 | } 113 | 114 | if (require.main === module) { 115 | startServers(); 116 | watchAll(); 117 | } else { 118 | module.exports.start = startServers; 119 | } 120 | -------------------------------------------------------------------------------- /bin/es3ify.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | var es3ify = require('es3ify'); 4 | return process.stdin.pipe(es3ify()).pipe(process.stdout); 5 | -------------------------------------------------------------------------------- /bin/test-browser.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var wd = require('wd'); 5 | var sauceConnectLauncher = require('sauce-connect-launcher'); 6 | var selenium = require('selenium-standalone'); 7 | var querystring = require("querystring"); 8 | 9 | var devserver = require('./dev-server.js'); 10 | 11 | var testTimeout = 30 * 60 * 1000; 12 | 13 | var username = process.env.SAUCE_USERNAME; 14 | var accessKey = process.env.SAUCE_ACCESS_KEY; 15 | 16 | // process.env.CLIENT is a colon seperated list of 17 | // (saucelabs|selenium):browserName:browserVerion:platform 18 | var tmp = (process.env.CLIENT || 'selenium:firefox').split(':'); 19 | var client = { 20 | runner: tmp[0] || 'selenium', 21 | browser: tmp[1] || 'firefox', 22 | version: tmp[2] || null, // Latest 23 | platform: tmp[3] || null 24 | }; 25 | 26 | var testUrl = 'http://127.0.0.1:8000/test/index.html'; 27 | if (typeof process.env.SUITE === 'string') { 28 | testUrl = 'http://127.0.0.1:8000/test/index-suite' + process.env.SUITE + 29 | '.html'; 30 | } 31 | var qs = {}; 32 | 33 | var sauceClient; 34 | var sauceConnectProcess; 35 | var tunnelId = process.env.TRAVIS_JOB_NUMBER || 'tunnel-' + Date.now(); 36 | 37 | if (client.runner === 'saucelabs') { 38 | qs.saucelabs = true; 39 | } 40 | if (process.env.GREP) { 41 | qs.grep = process.env.GREP; 42 | } 43 | testUrl += '?'; 44 | testUrl += querystring.stringify(qs); 45 | 46 | if (process.env.TRAVIS && 47 | client.browser !== 'firefox' && 48 | client.browser !== 'phantomjs' && 49 | process.env.TRAVIS_SECURE_ENV_VARS === 'false') { 50 | console.error('Not running test, cannot connect to saucelabs'); 51 | process.exit(1); 52 | return; 53 | } 54 | 55 | function testError(e) { 56 | console.error(e); 57 | console.error('Doh, tests failed'); 58 | sauceClient.quit(); 59 | process.exit(3); 60 | } 61 | 62 | function postResult(result) { 63 | process.exit(!process.env.PERF && result.failed ? 1 : 0); 64 | } 65 | 66 | function testComplete(result) { 67 | console.log(result); 68 | 69 | sauceClient.quit().then(function () { 70 | if (sauceConnectProcess) { 71 | sauceConnectProcess.close(function () { 72 | postResult(result); 73 | }); 74 | } else { 75 | postResult(result); 76 | } 77 | }); 78 | } 79 | 80 | function startSelenium(callback) { 81 | // Start selenium 82 | var opts = {}; 83 | selenium.install(opts, function(err) { 84 | if (err) { 85 | console.error('Failed to install selenium', err); 86 | process.exit(1); 87 | } 88 | selenium.start(opts, function(err, server) { 89 | sauceClient = wd.promiseChainRemote(); 90 | callback(); 91 | }); 92 | }); 93 | } 94 | 95 | function startSauceConnect(callback) { 96 | 97 | var options = { 98 | username: username, 99 | accessKey: accessKey, 100 | tunnelIdentifier: tunnelId 101 | }; 102 | 103 | sauceConnectLauncher(options, function (err, process) { 104 | if (err) { 105 | console.error('Failed to connect to saucelabs'); 106 | console.error(err); 107 | return process.exit(1); 108 | } 109 | sauceConnectProcess = process; 110 | sauceClient = wd.promiseChainRemote("localhost", 4445, username, accessKey); 111 | callback(); 112 | }); 113 | } 114 | 115 | function startTest() { 116 | 117 | console.log('Starting', client); 118 | 119 | var opts = { 120 | browserName: client.browser, 121 | version: client.version, 122 | platform: client.platform, 123 | tunnelTimeout: testTimeout, 124 | name: client.browser + ' - ' + tunnelId, 125 | 'max-duration': 60 * 30, 126 | 'command-timeout': 599, 127 | 'idle-timeout': 599, 128 | 'tunnel-identifier': tunnelId 129 | }; 130 | 131 | sauceClient.init(opts).get(testUrl, function () { 132 | 133 | /* jshint evil: true */ 134 | var interval = setInterval(function () { 135 | sauceClient.eval('window.results', function (err, results) { 136 | if (err) { 137 | clearInterval(interval); 138 | testError(err); 139 | } else if (results.completed || results.failures.length) { 140 | clearInterval(interval); 141 | testComplete(results); 142 | } else { 143 | console.log('=> ', results); 144 | } 145 | }); 146 | }, 10 * 1000); 147 | }); 148 | } 149 | 150 | devserver.start(function () { 151 | if (client.runner === 'saucelabs') { 152 | startSauceConnect(startTest); 153 | } else { 154 | startSelenium(startTest); 155 | } 156 | }); 157 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('../lib/client/core'); -------------------------------------------------------------------------------- /lib/client/core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../shared/utils'); 4 | var clientUtils = require('./utils'); 5 | var uuid = require('./../shared/uuid'); 6 | var errors = require('../shared/errors'); 7 | var log = require('debug')('pouchdb:worker:client'); 8 | var preprocessAttachments = clientUtils.preprocessAttachments; 9 | var encodeArgs = clientUtils.encodeArgs; 10 | var adapterFun = clientUtils.adapterFun; 11 | 12 | // Implements the PouchDB API for dealing with PouchDB instances over WW 13 | function WorkerPouch(opts, callback) { 14 | var api = this; 15 | 16 | if (typeof opts === 'string') { 17 | var slashIdx = utils.lastIndexOf(opts, '/'); 18 | opts = { 19 | url: opts.substring(0, slashIdx), 20 | name: opts.substring(slashIdx + 1) 21 | }; 22 | } else { 23 | opts = utils.clone(opts); 24 | } 25 | 26 | log('constructor called', opts); 27 | 28 | // Aspirational. once https://github.com/pouchdb/pouchdb/issues/5200 29 | // is resolved, you'll be able to directly pass in a worker here instead of 30 | // a function that returns a worker. 31 | var worker = (opts.worker && typeof opts.worker === 'function') ? 32 | opts.worker() : opts.worker; 33 | if (!worker || (!worker.postMessage && (!worker.controller || !worker.controller.postMessage))) { 34 | var workerOptsErrMessage = 35 | 'Error: you must provide a valid `worker` in `new PouchDB()`'; 36 | console.error(workerOptsErrMessage); 37 | return callback(new Error(workerOptsErrMessage)); 38 | } 39 | 40 | if (!opts.name) { 41 | var optsErrMessage = 'Error: you must provide a database name.'; 42 | console.error(optsErrMessage); 43 | return callback(new Error(optsErrMessage)); 44 | } 45 | 46 | function handleUncaughtError(content) { 47 | try { 48 | api.emit('error', content); 49 | } catch (err) { 50 | // TODO: it's weird that adapters should have to handle this themselves 51 | console.error( 52 | 'The user\'s map/reduce function threw an uncaught error.\n' + 53 | 'You can debug this error by doing:\n' + 54 | 'myDatabase.on(\'error\', function (err) { debugger; });\n' + 55 | 'Please double-check your map/reduce function.'); 56 | console.error(content); 57 | } 58 | } 59 | 60 | function onReceiveMessage(message) { 61 | var messageId = message.messageId; 62 | var messageType = message.type; 63 | var content = message.content; 64 | 65 | if (messageType === 'uncaughtError') { 66 | handleUncaughtError(content); 67 | return; 68 | } 69 | 70 | var cb = api._callbacks[messageId]; 71 | 72 | if (!cb) { 73 | log('duplicate message (ignoring)', messageId, messageType, content); 74 | return; 75 | } 76 | 77 | log('receive message', api._instanceId, messageId, messageType, content); 78 | 79 | if (messageType === 'error') { 80 | delete api._callbacks[messageId]; 81 | cb(content); 82 | } else if (messageType === 'success') { 83 | delete api._callbacks[messageId]; 84 | cb(null, content); 85 | } else { // 'update' 86 | api._changesListeners[messageId](content); 87 | } 88 | } 89 | 90 | function workerListener(e) { 91 | if (e.data.id === api._instanceId) { 92 | onReceiveMessage(e.data); 93 | } 94 | } 95 | 96 | function postMessage(message) { 97 | /* istanbul ignore if */ 98 | if (typeof worker.controller !== 'undefined') { 99 | // service worker, use MessageChannels because e.source is broken in Chrome < 51: 100 | // https://bugs.chromium.org/p/chromium/issues/detail?id=543198 101 | var channel = new MessageChannel(); 102 | channel.port1.onmessage = workerListener; 103 | worker.controller.postMessage(message, [channel.port2]); 104 | } else { 105 | // web worker 106 | worker.postMessage(message); 107 | } 108 | } 109 | 110 | function sendMessage(type, args, callback) { 111 | if (api._destroyed) { 112 | return callback(new Error('this db was destroyed')); 113 | } else if (api._closed) { 114 | return callback(new Error('this db was closed')); 115 | } 116 | var messageId = uuid(); 117 | log('send message', api._instanceId, messageId, type, args); 118 | api._callbacks[messageId] = callback; 119 | var encodedArgs = encodeArgs(args); 120 | postMessage({ 121 | id: api._instanceId, 122 | type: type, 123 | messageId: messageId, 124 | args: encodedArgs 125 | }); 126 | log('message sent', api._instanceId, messageId); 127 | } 128 | 129 | function sendRawMessage(messageId, type, args) { 130 | log('send message', api._instanceId, messageId, type, args); 131 | var encodedArgs = encodeArgs(args); 132 | postMessage({ 133 | id: api._instanceId, 134 | type: type, 135 | messageId: messageId, 136 | args: encodedArgs 137 | }); 138 | log('message sent', api._instanceId, messageId); 139 | } 140 | 141 | api.type = function () { 142 | return 'worker'; 143 | }; 144 | 145 | api._remote = false; 146 | 147 | api._id = adapterFun('id', function (callback) { 148 | sendMessage('id', [], callback); 149 | }); 150 | 151 | api.compact = adapterFun('compact', function (opts, callback) { 152 | if (typeof opts === 'function') { 153 | callback = opts; 154 | opts = {}; 155 | } 156 | sendMessage('compact', [opts], callback); 157 | }); 158 | 159 | api._info = function (callback) { 160 | sendMessage('info', [], callback); 161 | }; 162 | 163 | api.get = adapterFun('get', function (id, opts, callback) { 164 | if (typeof opts === 'function') { 165 | callback = opts; 166 | opts = {}; 167 | } 168 | sendMessage('get', [id, opts], callback); 169 | }); 170 | 171 | // hacky code necessary due to implicit breaking change in 172 | // https://github.com/pouchdb/pouchdb/commits/0ddeae6b 173 | api._get = function (id, opts, callback) { 174 | api.get(id, opts, function (err, doc) { 175 | if (err) { 176 | return callback(err); 177 | } 178 | callback(null, {doc: doc}); 179 | }); 180 | }; 181 | 182 | api.remove = 183 | adapterFun('remove', function (docOrId, optsOrRev, opts, callback) { 184 | var doc; 185 | if (typeof optsOrRev === 'string') { 186 | // id, rev, opts, callback style 187 | doc = { 188 | _id: docOrId, 189 | _rev: optsOrRev 190 | }; 191 | if (typeof opts === 'function') { 192 | callback = opts; 193 | opts = {}; 194 | } 195 | } else { 196 | // doc, opts, callback style 197 | doc = docOrId; 198 | if (typeof optsOrRev === 'function') { 199 | callback = optsOrRev; 200 | opts = {}; 201 | } else { 202 | callback = opts; 203 | opts = optsOrRev; 204 | } 205 | } 206 | var rev = (doc._rev || opts.rev); 207 | 208 | sendMessage('remove', [doc._id, rev], callback); 209 | }); 210 | 211 | api.getAttachment = 212 | adapterFun('getAttachment', function (docId, attachmentId, opts, 213 | callback) { 214 | if (typeof opts === 'function') { 215 | callback = opts; 216 | opts = {}; 217 | } 218 | sendMessage('getAttachment', [docId, attachmentId, opts], callback); 219 | }); 220 | 221 | api.removeAttachment = 222 | adapterFun('removeAttachment', function (docId, attachmentId, rev, 223 | callback) { 224 | 225 | sendMessage('removeAttachment', [docId, attachmentId, rev], callback); 226 | }); 227 | 228 | // Add the attachment given by blob and its contentType property 229 | // to the document with the given id, the revision given by rev, and 230 | // add it to the database given by host. 231 | api.putAttachment = 232 | adapterFun('putAttachment', function (docId, attachmentId, rev, blob, 233 | type, callback) { 234 | if (typeof type === 'function') { 235 | callback = type; 236 | type = blob; 237 | blob = rev; 238 | rev = null; 239 | } 240 | if (typeof type === 'undefined') { 241 | type = blob; 242 | blob = rev; 243 | rev = null; 244 | } 245 | 246 | if (typeof blob === 'string') { 247 | var binary; 248 | try { 249 | binary = atob(blob); 250 | } catch (err) { 251 | // it's not base64-encoded, so throw error 252 | return callback(errors.error(errors.BAD_ARG, 253 | 'Attachments need to be base64 encoded')); 254 | } 255 | blob = utils.createBlob([utils.binaryStringToArrayBuffer(binary)], {type: type}); 256 | } 257 | 258 | var args = [docId, attachmentId, rev, blob, type]; 259 | sendMessage('putAttachment', args, callback); 260 | }); 261 | 262 | api.put = adapterFun('put', utils.getArguments(function (args) { 263 | var temp, temptype, opts; 264 | var doc = args.shift(); 265 | var id = '_id' in doc; 266 | var callback = args.pop(); 267 | if (typeof doc !== 'object' || Array.isArray(doc)) { 268 | return callback(errors.error(errors.NOT_AN_OBJECT)); 269 | } 270 | 271 | doc = utils.clone(doc); 272 | 273 | preprocessAttachments(doc).then(function () { 274 | while (true) { 275 | temp = args.shift(); 276 | temptype = typeof temp; 277 | if (temptype === "string" && !id) { 278 | doc._id = temp; 279 | id = true; 280 | } else if (temptype === "string" && id && !('_rev' in doc)) { 281 | doc._rev = temp; 282 | } else if (temptype === "object") { 283 | opts = utils.clone(temp); 284 | } 285 | if (!args.length) { 286 | break; 287 | } 288 | } 289 | opts = opts || {}; 290 | 291 | sendMessage('put', [doc, opts], callback); 292 | }).catch(callback); 293 | 294 | })); 295 | 296 | api.post = adapterFun('post', function (doc, opts, callback) { 297 | if (typeof opts === 'function') { 298 | callback = opts; 299 | opts = {}; 300 | } 301 | opts = utils.clone(opts); 302 | 303 | sendMessage('post', [doc, opts], callback); 304 | }); 305 | 306 | api._bulkDocs = function (req, opts, callback) { 307 | sendMessage('bulkDocs', [req, opts], callback); 308 | }; 309 | 310 | api._allDocs = function (opts, callback) { 311 | if (typeof opts === 'function') { 312 | callback = opts; 313 | opts = {}; 314 | } 315 | sendMessage('allDocs', [opts], callback); 316 | }; 317 | 318 | api._changes = function (opts) { 319 | opts = utils.clone(opts); 320 | 321 | if (opts.continuous) { 322 | var messageId = uuid(); 323 | api._changesListeners[messageId] = opts.onChange; 324 | api._callbacks[messageId] = opts.complete; 325 | sendRawMessage(messageId, 'liveChanges', [opts]); 326 | return { 327 | cancel: function () { 328 | sendRawMessage(messageId, 'cancelChanges', []); 329 | } 330 | }; 331 | } 332 | 333 | sendMessage('changes', [opts], function (err, res) { 334 | if (err) { 335 | opts.complete(err); 336 | return callback(err); 337 | } 338 | res.results.forEach(function (change) { 339 | opts.onChange(change); 340 | }); 341 | if (opts.returnDocs === false || opts.return_docs === false) { 342 | res.results = []; 343 | } 344 | opts.complete(null, res); 345 | }); 346 | }; 347 | 348 | // Given a set of document/revision IDs (given by req), tets the subset of 349 | // those that do NOT correspond to revisions stored in the database. 350 | // See http://wiki.apache.org/couchdb/HttpPostRevsDiff 351 | api.revsDiff = adapterFun('revsDiff', function (req, opts, callback) { 352 | // If no options were given, set the callback to be the second parameter 353 | if (typeof opts === 'function') { 354 | callback = opts; 355 | opts = {}; 356 | } 357 | 358 | sendMessage('revsDiff', [req, opts], callback); 359 | }); 360 | 361 | api._query = adapterFun('query', function (fun, opts, callback) { 362 | if (typeof opts === 'function') { 363 | callback = opts; 364 | opts = {}; 365 | } 366 | var funEncoded = fun; 367 | if (typeof fun === 'function') { 368 | funEncoded = {map: fun}; 369 | } 370 | sendMessage('query', [funEncoded, opts], callback); 371 | }); 372 | 373 | api._viewCleanup = adapterFun('viewCleanup', function (callback) { 374 | sendMessage('viewCleanup', [], callback); 375 | }); 376 | 377 | api._close = function (callback) { 378 | api._closed = true; 379 | callback(); 380 | }; 381 | 382 | api.destroy = adapterFun('destroy', function (opts, callback) { 383 | if (typeof opts === 'function') { 384 | callback = opts; 385 | opts = {}; 386 | } 387 | sendMessage('destroy', [], function (err, res) { 388 | if (err) { 389 | api.emit('error', err); 390 | return callback(err); 391 | } 392 | api._destroyed = true; 393 | worker.removeEventListener('message', workerListener); 394 | api.emit('destroyed'); 395 | callback(null, res); 396 | }); 397 | }); 398 | 399 | // api.name was added in pouchdb 6.0.0 400 | api._instanceId = api.name || opts.originalName; 401 | api._callbacks = {}; 402 | api._changesListeners = {}; 403 | 404 | worker.addEventListener('message', workerListener); 405 | 406 | var workerOpts = { 407 | name: api._instanceId, 408 | auto_compaction: !!opts.auto_compaction, 409 | storage: opts.storage 410 | }; 411 | if (opts.revs_limit) { 412 | workerOpts.revs_limit = opts.revs_limit; 413 | } 414 | 415 | sendMessage('createDatabase', [workerOpts], function (err) { 416 | if (err) { 417 | return callback(err); 418 | } 419 | callback(null, api); 420 | }); 421 | } 422 | 423 | // WorkerPouch is a valid adapter. 424 | WorkerPouch.valid = function () { 425 | return true; 426 | }; 427 | WorkerPouch.use_prefix = false; 428 | 429 | module.exports = WorkerPouch; 430 | -------------------------------------------------------------------------------- /lib/client/create-worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global webkitURL */ 3 | 4 | module.exports = function createWorker(code) { 5 | var createBlob = require('../shared/utils').createBlob; 6 | var URLCompat = typeof URL !== 'undefined' ? URL : webkitURL; 7 | 8 | function makeBlobURI(script) { 9 | var blob = createBlob([script], {type: 'text/javascript'}); 10 | return URLCompat.createObjectURL(blob); 11 | } 12 | 13 | var blob = createBlob([code], {type: 'text/javascript'}); 14 | return new Worker(makeBlobURI(blob)); 15 | }; -------------------------------------------------------------------------------- /lib/client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // main script used with a blob-style worker 4 | 5 | var extend = require('js-extend').extend; 6 | var WorkerPouchCore = require('./core'); 7 | var createWorker = require('./create-worker'); 8 | var isSupportedBrowser = require('./is-supported-browser'); 9 | var workerCode = require('../workerified'); 10 | 11 | function WorkerPouch(opts, callback) { 12 | 13 | var worker = window.__pouchdb_global_worker; // cache so there's only one 14 | if (!worker) { 15 | try { 16 | worker = createWorker(workerCode); 17 | worker.addEventListener('error', function (e) { 18 | if ('console' in global && 'warn' in console) { 19 | console.warn('worker threw an error', e.error); 20 | } 21 | }); 22 | window.__pouchdb_global_worker = worker; 23 | } catch (e) { 24 | if ('console' in global && 'info' in console) { 25 | console.info('This browser is not supported by WorkerPouch. ' + 26 | 'Please use isSupportedBrowser() to check.', e); 27 | } 28 | return callback(new Error('browser unsupported by worker-pouch')); 29 | } 30 | } 31 | 32 | var _opts = extend({ 33 | worker: function () { return worker; } 34 | }, opts); 35 | 36 | WorkerPouchCore.call(this, _opts, callback); 37 | } 38 | 39 | WorkerPouch.valid = function () { 40 | return true; 41 | }; 42 | WorkerPouch.use_prefix = false; 43 | 44 | WorkerPouch.isSupportedBrowser = isSupportedBrowser; 45 | 46 | module.exports = WorkerPouch; 47 | 48 | /* istanbul ignore next */ 49 | if (typeof window !== 'undefined' && window.PouchDB) { 50 | window.PouchDB.adapter('worker', module.exports); 51 | } 52 | -------------------------------------------------------------------------------- /lib/client/is-supported-browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _interopDefault (ex) { 4 | return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; 5 | } 6 | 7 | var Promise = _interopDefault(require('pouchdb-promise')); 8 | var createWorker = require('./create-worker'); 9 | 10 | module.exports = function isSupportedBrowser() { 11 | return Promise.resolve().then(function () { 12 | // synchronously throws in IE/Edge 13 | var worker = createWorker('' + 14 | 'self.onmessage = function () {' + 15 | ' self.postMessage({' + 16 | ' hasIndexedDB: (typeof indexedDB !== "undefined")' + 17 | ' });' + 18 | '};'); 19 | 20 | return new Promise(function (resolve, reject) { 21 | 22 | function listener(e) { 23 | worker.terminate(); 24 | if (e.data.hasIndexedDB) { 25 | resolve(); 26 | return; 27 | } 28 | reject(); 29 | } 30 | 31 | function errorListener() { 32 | worker.terminate(); 33 | reject(); 34 | } 35 | 36 | worker.addEventListener('error', errorListener); 37 | worker.addEventListener('message', listener); 38 | worker.postMessage({}); 39 | }); 40 | }).then(function () { 41 | return true; 42 | }, function (err) { 43 | if ('console' in global && 'info' in console) { 44 | console.info('This browser is not supported by WorkerPouch', err); 45 | } 46 | return false; 47 | }); 48 | }; -------------------------------------------------------------------------------- /lib/client/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../shared/utils'); 4 | var log = require('debug')('pouchdb:worker:client'); 5 | var isBrowser = typeof process === 'undefined' || process.browser; 6 | 7 | exports.preprocessAttachments = function preprocessAttachments(doc) { 8 | if (!doc._attachments || !Object.keys(doc._attachments)) { 9 | return utils.Promise.resolve(); 10 | } 11 | 12 | return utils.Promise.all(Object.keys(doc._attachments).map(function (key) { 13 | var attachment = doc._attachments[key]; 14 | if (attachment.data && typeof attachment.data !== 'string') { 15 | if (isBrowser) { 16 | return new utils.Promise(function (resolve) { 17 | utils.readAsBinaryString(attachment.data, function (binary) { 18 | attachment.data = btoa(binary); 19 | resolve(); 20 | }); 21 | }); 22 | } else { 23 | attachment.data = attachment.data.toString('base64'); 24 | } 25 | } 26 | })); 27 | }; 28 | 29 | function encodeObjectArg(arg) { 30 | // these can't be encoded by normal structured cloning 31 | var funcKeys = ['filter', 'map', 'reduce']; 32 | var keysToRemove = ['onChange', 'processChange', 'complete']; 33 | var clonedArg = {}; 34 | Object.keys(arg).forEach(function (key) { 35 | if (keysToRemove.indexOf(key) !== -1) { 36 | return; 37 | } 38 | if (funcKeys.indexOf(key) !== -1 && typeof arg[key] === 'function') { 39 | clonedArg[key] = { 40 | type: 'func', 41 | func: arg[key].toString() 42 | }; 43 | } else { 44 | clonedArg[key] = arg[key]; 45 | } 46 | }); 47 | return clonedArg; 48 | } 49 | 50 | exports.encodeArgs = function encodeArgs(args) { 51 | var result = []; 52 | args.forEach(function (arg) { 53 | if (arg === null || typeof arg !== 'object' || 54 | Array.isArray(arg) || arg instanceof Blob || arg instanceof Date) { 55 | result.push(arg); 56 | } else { 57 | result.push(encodeObjectArg(arg)); 58 | } 59 | }); 60 | return result; 61 | }; 62 | 63 | exports.padInt = function padInt(i, len) { 64 | var res = i.toString(); 65 | while (res.length < len) { 66 | res = '0' + res; 67 | } 68 | return res; 69 | }; 70 | 71 | 72 | exports.adapterFun = function adapterFun(name, callback) { 73 | 74 | function logApiCall(self, name, args) { 75 | if (!log.enabled) { 76 | return; 77 | } 78 | // db.name was added in pouch 6.0.0 79 | var dbName = self.name || self._db_name; 80 | var logArgs = [dbName, name]; 81 | for (var i = 0; i < args.length - 1; i++) { 82 | logArgs.push(args[i]); 83 | } 84 | log.apply(null, logArgs); 85 | 86 | // override the callback itself to log the response 87 | var origCallback = args[args.length - 1]; 88 | args[args.length - 1] = function (err, res) { 89 | var responseArgs = [dbName, name]; 90 | responseArgs = responseArgs.concat( 91 | err ? ['error', err] : ['success', res] 92 | ); 93 | log.apply(null, responseArgs); 94 | origCallback(err, res); 95 | }; 96 | } 97 | 98 | 99 | return utils.toPromise(utils.getArguments(function (args) { 100 | if (this._closed) { 101 | return utils.Promise.reject(new Error('database is closed')); 102 | } 103 | var self = this; 104 | logApiCall(self, name, args); 105 | if (!this.taskqueue.isReady) { 106 | return new utils.Promise(function (fulfill, reject) { 107 | self.taskqueue.addTask(function (failed) { 108 | if (failed) { 109 | reject(failed); 110 | } else { 111 | fulfill(self[name].apply(self, args)); 112 | } 113 | }); 114 | }); 115 | } 116 | return callback.apply(this, args); 117 | })); 118 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./client'); -------------------------------------------------------------------------------- /lib/shared/errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var inherits = require('inherits'); 4 | inherits(PouchError, Error); 5 | 6 | function PouchError(opts) { 7 | Error.call(opts.reason); 8 | this.status = opts.status; 9 | this.name = opts.error; 10 | this.message = opts.reason; 11 | this.error = true; 12 | } 13 | 14 | PouchError.prototype.toString = function () { 15 | return JSON.stringify({ 16 | status: this.status, 17 | name: this.name, 18 | message: this.message 19 | }); 20 | }; 21 | 22 | exports.UNAUTHORIZED = new PouchError({ 23 | status: 401, 24 | error: 'unauthorized', 25 | reason: "Name or password is incorrect." 26 | }); 27 | 28 | exports.MISSING_BULK_DOCS = new PouchError({ 29 | status: 400, 30 | error: 'bad_request', 31 | reason: "Missing JSON list of 'docs'" 32 | }); 33 | 34 | exports.MISSING_DOC = new PouchError({ 35 | status: 404, 36 | error: 'not_found', 37 | reason: 'missing' 38 | }); 39 | 40 | exports.REV_CONFLICT = new PouchError({ 41 | status: 409, 42 | error: 'conflict', 43 | reason: 'Document update conflict' 44 | }); 45 | 46 | exports.INVALID_ID = new PouchError({ 47 | status: 400, 48 | error: 'invalid_id', 49 | reason: '_id field must contain a string' 50 | }); 51 | 52 | exports.MISSING_ID = new PouchError({ 53 | status: 412, 54 | error: 'missing_id', 55 | reason: '_id is required for puts' 56 | }); 57 | 58 | exports.RESERVED_ID = new PouchError({ 59 | status: 400, 60 | error: 'bad_request', 61 | reason: 'Only reserved document ids may start with underscore.' 62 | }); 63 | 64 | exports.NOT_OPEN = new PouchError({ 65 | status: 412, 66 | error: 'precondition_failed', 67 | reason: 'Database not open' 68 | }); 69 | 70 | exports.UNKNOWN_ERROR = new PouchError({ 71 | status: 500, 72 | error: 'unknown_error', 73 | reason: 'Database encountered an unknown error' 74 | }); 75 | 76 | exports.BAD_ARG = new PouchError({ 77 | status: 500, 78 | error: 'badarg', 79 | reason: 'Some query argument is invalid' 80 | }); 81 | 82 | exports.INVALID_REQUEST = new PouchError({ 83 | status: 400, 84 | error: 'invalid_request', 85 | reason: 'Request was invalid' 86 | }); 87 | 88 | exports.QUERY_PARSE_ERROR = new PouchError({ 89 | status: 400, 90 | error: 'query_parse_error', 91 | reason: 'Some query parameter is invalid' 92 | }); 93 | 94 | exports.DOC_VALIDATION = new PouchError({ 95 | status: 500, 96 | error: 'doc_validation', 97 | reason: 'Bad special document member' 98 | }); 99 | 100 | exports.BAD_REQUEST = new PouchError({ 101 | status: 400, 102 | error: 'bad_request', 103 | reason: 'Something wrong with the request' 104 | }); 105 | 106 | exports.NOT_AN_OBJECT = new PouchError({ 107 | status: 400, 108 | error: 'bad_request', 109 | reason: 'Document must be a JSON object' 110 | }); 111 | 112 | exports.DB_MISSING = new PouchError({ 113 | status: 404, 114 | error: 'not_found', 115 | reason: 'Database not found' 116 | }); 117 | 118 | exports.IDB_ERROR = new PouchError({ 119 | status: 500, 120 | error: 'indexed_db_went_bad', 121 | reason: 'unknown' 122 | }); 123 | 124 | exports.WSQ_ERROR = new PouchError({ 125 | status: 500, 126 | error: 'web_sql_went_bad', 127 | reason: 'unknown' 128 | }); 129 | 130 | exports.LDB_ERROR = new PouchError({ 131 | status: 500, 132 | error: 'levelDB_went_went_bad', 133 | reason: 'unknown' 134 | }); 135 | 136 | exports.FORBIDDEN = new PouchError({ 137 | status: 403, 138 | error: 'forbidden', 139 | reason: 'Forbidden by design doc validate_doc_update function' 140 | }); 141 | 142 | exports.INVALID_REV = new PouchError({ 143 | status: 400, 144 | error: 'bad_request', 145 | reason: 'Invalid rev format' 146 | }); 147 | 148 | exports.FILE_EXISTS = new PouchError({ 149 | status: 412, 150 | error: 'file_exists', 151 | reason: 'The database could not be created, the file already exists.' 152 | }); 153 | 154 | exports.MISSING_STUB = new PouchError({ 155 | status: 412, 156 | error: 'missing_stub' 157 | }); 158 | 159 | exports.error = function (error, reason, name) { 160 | function CustomPouchError(reason) { 161 | // inherit error properties from our parent error manually 162 | // so as to allow proper JSON parsing. 163 | /* jshint ignore:start */ 164 | for (var p in error) { 165 | if (typeof error[p] !== 'function') { 166 | this[p] = error[p]; 167 | } 168 | } 169 | /* jshint ignore:end */ 170 | if (name !== undefined) { 171 | this.name = name; 172 | } 173 | if (reason !== undefined) { 174 | this.reason = reason; 175 | } 176 | } 177 | CustomPouchError.prototype = PouchError.prototype; 178 | return new CustomPouchError(reason); 179 | }; 180 | 181 | // Find one of the errors defined above based on the value 182 | // of the specified property. 183 | // If reason is provided prefer the error matching that reason. 184 | // This is for differentiating between errors with the same name and status, 185 | // eg, bad_request. 186 | exports.getErrorTypeByProp = function (prop, value, reason) { 187 | var errors = exports; 188 | var keys = Object.keys(errors).filter(function (key) { 189 | var error = errors[key]; 190 | return typeof error !== 'function' && error[prop] === value; 191 | }); 192 | var key = reason && keys.filter(function (key) { 193 | var error = errors[key]; 194 | return error.message === reason; 195 | })[0] || keys[0]; 196 | return (key) ? errors[key] : null; 197 | }; 198 | 199 | exports.generateErrorFromResponse = function (res) { 200 | var error, errName, errType, errMsg, errReason; 201 | var errors = exports; 202 | 203 | errName = (res.error === true && typeof res.name === 'string') ? 204 | res.name : 205 | res.error; 206 | errReason = res.reason; 207 | errType = errors.getErrorTypeByProp('name', errName, errReason); 208 | 209 | if (res.missing || 210 | errReason === 'missing' || 211 | errReason === 'deleted' || 212 | errName === 'not_found') { 213 | errType = errors.MISSING_DOC; 214 | } else if (errName === 'doc_validation') { 215 | // doc validation needs special treatment since 216 | // res.reason depends on the validation error. 217 | // see utils.js 218 | errType = errors.DOC_VALIDATION; 219 | errMsg = errReason; 220 | } else if (errName === 'bad_request' && errType.message !== errReason) { 221 | // if bad_request error already found based on reason don't override. 222 | 223 | // attachment errors. 224 | if (errReason.indexOf('unknown stub attachment') === 0) { 225 | errType = errors.MISSING_STUB; 226 | errMsg = errReason; 227 | } else { 228 | errType = errors.BAD_REQUEST; 229 | } 230 | } 231 | 232 | // fallback to error by statys or unknown error. 233 | if (!errType) { 234 | errType = errors.getErrorTypeByProp('status', res.status, errReason) || 235 | errors.UNKNOWN_ERROR; 236 | } 237 | 238 | error = errors.error(errType, errReason, errName); 239 | 240 | // Keep custom message. 241 | if (errMsg) { 242 | error.message = errMsg; 243 | } 244 | 245 | // Keep helpful response data in our error messages. 246 | if (res.id) { 247 | error.id = res.id; 248 | } 249 | if (res.status) { 250 | error.status = res.status; 251 | } 252 | if (res.statusText) { 253 | error.name = res.statusText; 254 | } 255 | if (res.missing) { 256 | error.missing = res.missing; 257 | } 258 | 259 | return error; 260 | }; 261 | -------------------------------------------------------------------------------- /lib/shared/pouchdb-clone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isBinaryObject(object) { 4 | return object instanceof ArrayBuffer || 5 | (typeof Blob !== 'undefined' && object instanceof Blob); 6 | } 7 | 8 | function cloneArrayBuffer(buff) { 9 | if (typeof buff.slice === 'function') { 10 | return buff.slice(0); 11 | } 12 | // IE10-11 slice() polyfill 13 | var target = new ArrayBuffer(buff.byteLength); 14 | var targetArray = new Uint8Array(target); 15 | var sourceArray = new Uint8Array(buff); 16 | targetArray.set(sourceArray); 17 | return target; 18 | } 19 | 20 | function cloneBinaryObject(object) { 21 | if (object instanceof ArrayBuffer) { 22 | return cloneArrayBuffer(object); 23 | } 24 | // Blob 25 | return object.slice(0, object.size, object.type); 26 | } 27 | 28 | module.exports = function clone(object) { 29 | var newObject; 30 | var i; 31 | var len; 32 | 33 | if (!object || typeof object !== 'object') { 34 | return object; 35 | } 36 | 37 | if (Array.isArray(object)) { 38 | newObject = []; 39 | for (i = 0, len = object.length; i < len; i++) { 40 | newObject[i] = clone(object[i]); 41 | } 42 | return newObject; 43 | } 44 | 45 | // special case: to avoid inconsistencies between IndexedDB 46 | // and other backends, we automatically stringify Dates 47 | if (object instanceof Date) { 48 | return object.toISOString(); 49 | } 50 | 51 | if (isBinaryObject(object)) { 52 | return cloneBinaryObject(object); 53 | } 54 | 55 | newObject = {}; 56 | for (i in object) { 57 | if (Object.prototype.hasOwnProperty.call(object, i)) { 58 | var value = clone(object[i]); 59 | if (typeof value !== 'undefined') { 60 | newObject[i] = value; 61 | } 62 | } 63 | } 64 | return newObject; 65 | }; 66 | -------------------------------------------------------------------------------- /lib/shared/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _interopDefault (ex) { 4 | return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; 5 | } 6 | 7 | var Promise = _interopDefault(require('pouchdb-promise')); 8 | 9 | exports.lastIndexOf = function lastIndexOf(str, char) { 10 | for (var i = str.length - 1; i >= 0; i--) { 11 | if (str.charAt(i) === char) { 12 | return i; 13 | } 14 | } 15 | return -1; 16 | }; 17 | 18 | exports.clone = require('./pouchdb-clone'); 19 | 20 | /* istanbul ignore next */ 21 | exports.once = function once(fun) { 22 | var called = false; 23 | return exports.getArguments(function (args) { 24 | if (called) { 25 | if ('console' in global && 'trace' in console) { 26 | console.trace(); 27 | } 28 | throw new Error('once called more than once'); 29 | } else { 30 | called = true; 31 | fun.apply(this, args); 32 | } 33 | }); 34 | }; 35 | /* istanbul ignore next */ 36 | exports.getArguments = function getArguments(fun) { 37 | return function () { 38 | var len = arguments.length; 39 | var args = new Array(len); 40 | var i = -1; 41 | while (++i < len) { 42 | args[i] = arguments[i]; 43 | } 44 | return fun.call(this, args); 45 | }; 46 | }; 47 | /* istanbul ignore next */ 48 | exports.toPromise = function toPromise(func) { 49 | //create the function we will be returning 50 | return exports.getArguments(function (args) { 51 | var self = this; 52 | var tempCB = (typeof args[args.length - 1] === 'function') ? args.pop() : false; 53 | // if the last argument is a function, assume its a callback 54 | var usedCB; 55 | if (tempCB) { 56 | // if it was a callback, create a new callback which calls it, 57 | // but do so async so we don't trap any errors 58 | usedCB = function (err, resp) { 59 | process.nextTick(function () { 60 | tempCB(err, resp); 61 | }); 62 | }; 63 | } 64 | var promise = new Promise(function (fulfill, reject) { 65 | try { 66 | var callback = exports.once(function (err, mesg) { 67 | if (err) { 68 | reject(err); 69 | } else { 70 | fulfill(mesg); 71 | } 72 | }); 73 | // create a callback for this invocation 74 | // apply the function in the orig context 75 | args.push(callback); 76 | func.apply(self, args); 77 | } catch (e) { 78 | reject(e); 79 | } 80 | }); 81 | // if there is a callback, call it back 82 | if (usedCB) { 83 | promise.then(function (result) { 84 | usedCB(null, result); 85 | }, usedCB); 86 | } 87 | promise.cancel = function () { 88 | return this; 89 | }; 90 | return promise; 91 | }); 92 | }; 93 | 94 | exports.inherits = require('inherits'); 95 | exports.Promise = Promise; 96 | 97 | var binUtil = require('pouchdb-binary-util'); 98 | 99 | exports.createBlob = binUtil.createBlob; 100 | exports.readAsArrayBuffer = binUtil.readAsArrayBuffer; 101 | exports.readAsBinaryString = binUtil.readAsBinaryString; 102 | exports.binaryStringToArrayBuffer = binUtil.binaryStringToArrayBuffer; 103 | exports.arrayBufferToBinaryString = binUtil.arrayBufferToBinaryString; 104 | -------------------------------------------------------------------------------- /lib/shared/uuid.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // BEGIN Math.uuid.js 4 | 5 | /*! 6 | Math.uuid.js (v1.4) 7 | http://www.broofa.com 8 | mailto:robert@broofa.com 9 | 10 | Copyright (c) 2010 Robert Kieffer 11 | Dual licensed under the MIT and GPL licenses. 12 | */ 13 | 14 | /* 15 | * Generate a random uuid. 16 | * 17 | * USAGE: Math.uuid(length, radix) 18 | * length - the desired number of characters 19 | * radix - the number of allowable values for each character. 20 | * 21 | * EXAMPLES: 22 | * // No arguments - returns RFC4122, version 4 ID 23 | * >>> Math.uuid() 24 | * "92329D39-6F5C-4520-ABFC-AAB64544E172" 25 | * 26 | * // One argument - returns ID of the specified length 27 | * >>> Math.uuid(15) // 15 character ID (default base=62) 28 | * "VcydxgltxrVZSTV" 29 | * 30 | * // Two arguments - returns ID of the specified length, and radix. 31 | * // (Radix must be <= 62) 32 | * >>> Math.uuid(8, 2) // 8 character ID (base=2) 33 | * "01001010" 34 | * >>> Math.uuid(8, 10) // 8 character ID (base=10) 35 | * "47473046" 36 | * >>> Math.uuid(8, 16) // 8 character ID (base=16) 37 | * "098F4D35" 38 | */ 39 | var chars = ( 40 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 41 | 'abcdefghijklmnopqrstuvwxyz' 42 | ).split(''); 43 | function getValue(radix) { 44 | return 0 | Math.random() * radix; 45 | } 46 | function uuid(len, radix) { 47 | radix = radix || chars.length; 48 | var out = ''; 49 | var i = -1; 50 | 51 | if (len) { 52 | // Compact form 53 | while (++i < len) { 54 | out += chars[getValue(radix)]; 55 | } 56 | return out; 57 | } 58 | // rfc4122, version 4 form 59 | // Fill in random data. At i==19 set the high bits of clock sequence as 60 | // per rfc4122, sec. 4.1.5 61 | while (++i < 36) { 62 | switch (i) { 63 | case 8: 64 | case 13: 65 | case 18: 66 | case 23: 67 | out += '-'; 68 | break; 69 | case 19: 70 | out += chars[(getValue(16) & 0x3) | 0x8]; 71 | break; 72 | default: 73 | out += chars[getValue(16)]; 74 | } 75 | } 76 | 77 | return out; 78 | } 79 | 80 | 81 | 82 | module.exports = uuid; 83 | 84 | -------------------------------------------------------------------------------- /lib/worker/core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint worker:true */ 4 | 5 | function _interopDefault (ex) { 6 | return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; 7 | } 8 | 9 | var Promise = _interopDefault(require('pouchdb-promise')); 10 | var errors = require('../shared/errors'); 11 | var workerUtils = require('./utils'); 12 | var decodeArgs = workerUtils.decodeArgs; 13 | var dbs = {}; 14 | var allChanges = {}; 15 | 16 | var log = require('debug')('pouchdb:worker'); 17 | 18 | function registerWorkerPouch(self, pouchCreator) { 19 | 20 | function postMessage(msg, event) { 21 | if (typeof self.postMessage !== 'function') { // service worker 22 | event.ports[0].postMessage(msg); 23 | } else { // web worker 24 | self.postMessage(msg); 25 | } 26 | } 27 | 28 | function sendUncaughtError(clientId, data, event) { 29 | log(' -> sendUncaughtError', clientId, data); 30 | postMessage({ 31 | type: 'uncaughtError', 32 | id: clientId, 33 | content: workerUtils.createError(data) 34 | }, event); 35 | } 36 | 37 | function sendError(clientId, messageId, data, event) { 38 | log(' -> sendError', clientId, messageId, data); 39 | postMessage({ 40 | type: 'error', 41 | id: clientId, 42 | messageId: messageId, 43 | content: workerUtils.createError(data) 44 | }, event); 45 | } 46 | 47 | function sendSuccess(clientId, messageId, data, event) { 48 | log(' -> sendSuccess', clientId, messageId); 49 | postMessage({ 50 | type: 'success', 51 | id: clientId, 52 | messageId: messageId, 53 | content: data 54 | }, event); 55 | } 56 | 57 | function sendUpdate(clientId, messageId, data, event) { 58 | log(' -> sendUpdate', clientId, messageId); 59 | postMessage({ 60 | type: 'update', 61 | id: clientId, 62 | messageId: messageId, 63 | content: data 64 | }, event); 65 | } 66 | 67 | function dbMethod(clientId, methodName, messageId, args, event) { 68 | var db = dbs['$' + clientId]; 69 | if (!db) { 70 | return sendError(clientId, messageId, {error: 'db not found'}, event); 71 | } 72 | Promise.resolve().then(function () { 73 | return db[methodName].apply(db, args); 74 | }).then(function (res) { 75 | sendSuccess(clientId, messageId, res, event); 76 | }).catch(function (err) { 77 | sendError(clientId, messageId, err, event); 78 | }); 79 | } 80 | 81 | function changes(clientId, messageId, args, event) { 82 | var opts = args[0]; 83 | if (opts && typeof opts === 'object') { 84 | // just send all the docs anyway because we need to emit change events 85 | // TODO: be smarter about emitting changes without building up an array 86 | opts.returnDocs = true; 87 | opts.return_docs = true; 88 | } 89 | dbMethod(clientId, 'changes', messageId, args, event); 90 | } 91 | 92 | function getAttachment(clientId, messageId, args, event) { 93 | var db = dbs['$' + clientId]; 94 | if (!db) { 95 | return sendError(clientId, messageId, {error: 'db not found'}, event); 96 | } 97 | 98 | Promise.resolve().then(function () { 99 | var docId = args[0]; 100 | var attId = args[1]; 101 | var opts = args[2]; 102 | if (typeof opts !== 'object') { 103 | opts = {}; 104 | } 105 | return db.get(docId, opts).then(function (doc) { 106 | if (!doc._attachments || !doc._attachments[attId]) { 107 | throw errors.MISSING_DOC; 108 | } 109 | return db.getAttachment.apply(db, args).then(function (buff) { 110 | sendSuccess(clientId, messageId, buff, event); 111 | }); 112 | }); 113 | }).catch(function (err) { 114 | sendError(clientId, messageId, err, event); 115 | }); 116 | } 117 | 118 | function destroy(clientId, messageId, args, event) { 119 | var key = '$' + clientId; 120 | var db = dbs[key]; 121 | if (!db) { 122 | return sendError(clientId, messageId, {error: 'db not found'}, event); 123 | } 124 | delete dbs[key]; 125 | Promise.resolve().then(function () { 126 | return db.destroy.apply(db, args); 127 | }).then(function (res) { 128 | sendSuccess(clientId, messageId, res, event); 129 | }).catch(function (err) { 130 | sendError(clientId, messageId, err, event); 131 | }); 132 | } 133 | 134 | function liveChanges(clientId, messageId, args, event) { 135 | var db = dbs['$' + clientId]; 136 | if (!db) { 137 | return sendError(clientId, messageId, {error: 'db not found'}, event); 138 | } 139 | Promise.resolve().then(function () { 140 | var changes = db.changes(args[0]); 141 | allChanges[messageId] = changes; 142 | changes.on('change', function (change) { 143 | sendUpdate(clientId, messageId, change, event); 144 | }).on('complete', function (change) { 145 | changes.removeAllListeners(); 146 | delete allChanges[messageId]; 147 | sendSuccess(clientId, messageId, change, event); 148 | }).on('error', function (change) { 149 | changes.removeAllListeners(); 150 | delete allChanges[messageId]; 151 | sendError(clientId, messageId, change, event); 152 | }); 153 | }); 154 | } 155 | 156 | function cancelChanges(messageId) { 157 | var changes = allChanges[messageId]; 158 | if (changes) { 159 | changes.cancel(); 160 | } 161 | } 162 | 163 | function addUncaughtErrorHandler(db, clientId, event) { 164 | return Promise.resolve().then(function () { 165 | db.on('error', function (err) { 166 | sendUncaughtError(clientId, err, event); 167 | }); 168 | }); 169 | } 170 | 171 | function createDatabase(clientId, messageId, args, event) { 172 | var key = '$' + clientId; 173 | var db = dbs[key]; 174 | if (db) { 175 | return addUncaughtErrorHandler(db, clientId, event).then(function () { 176 | return sendSuccess(clientId, messageId, {ok: true, exists: true}, event); 177 | }); 178 | } 179 | 180 | var name = typeof args[0] === 'string' ? args[0] : args[0].name; 181 | 182 | if (!name) { 183 | return sendError(clientId, messageId, { 184 | error: 'you must provide a database name' 185 | }, event); 186 | } 187 | 188 | db = dbs[key] = pouchCreator(args[0]); 189 | addUncaughtErrorHandler(db, clientId, event).then(function () { 190 | sendSuccess(clientId, messageId, {ok: true}, event); 191 | }).catch(function (err) { 192 | sendError(clientId, messageId, err, event); 193 | }); 194 | } 195 | 196 | function onReceiveMessage(clientId, type, messageId, args, event) { 197 | log('onReceiveMessage', type, clientId, messageId, args, event); 198 | switch (type) { 199 | case 'createDatabase': 200 | return createDatabase(clientId, messageId, args, event); 201 | case 'id': 202 | case 'info': 203 | case 'put': 204 | case 'allDocs': 205 | case 'bulkDocs': 206 | case 'post': 207 | case 'get': 208 | case 'remove': 209 | case 'revsDiff': 210 | case 'compact': 211 | case 'viewCleanup': 212 | case 'removeAttachment': 213 | case 'putAttachment': 214 | case 'query': 215 | return dbMethod(clientId, type, messageId, args, event); 216 | case 'changes': 217 | return changes(clientId, messageId, args, event); 218 | case 'getAttachment': 219 | return getAttachment(clientId, messageId, args, event); 220 | case 'liveChanges': 221 | return liveChanges(clientId, messageId, args, event); 222 | case 'cancelChanges': 223 | return cancelChanges(messageId); 224 | case 'destroy': 225 | return destroy(clientId, messageId, args, event); 226 | default: 227 | return sendError(clientId, messageId, {error: 'unknown API method: ' + type}, event); 228 | } 229 | } 230 | 231 | function handleMessage(message, clientId, event) { 232 | var type = message.type; 233 | var messageId = message.messageId; 234 | var args = decodeArgs(message.args); 235 | onReceiveMessage(clientId, type, messageId, args, event); 236 | } 237 | 238 | self.addEventListener('message', function (event) { 239 | if (!event.data || !event.data.id || !event.data.args || 240 | !event.data.type || !event.data.messageId) { 241 | // assume this is not a message from worker-pouch 242 | // (e.g. the user is using the custom API instead) 243 | return; 244 | } 245 | var clientId = event.data.id; 246 | if (event.data.type === 'close') { 247 | log('closing worker', clientId); 248 | delete dbs['$' + clientId]; 249 | } else { 250 | handleMessage(event.data, clientId, event); 251 | } 252 | }); 253 | } 254 | 255 | module.exports = registerWorkerPouch; 256 | -------------------------------------------------------------------------------- /lib/worker/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint worker:true */ 4 | 5 | // This file gets workerified. It's the main worker script used in blob URLs. 6 | var registerWorkerPouch = require('./core'); 7 | var PouchDB = require('./pouchdb-idb-only'); 8 | registerWorkerPouch(self, PouchDB); -------------------------------------------------------------------------------- /lib/worker/pouchdb-idb-only.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // no need for WebSQL when running in a worker 4 | module.exports = require('pouchdb-core') 5 | .plugin(require('pouchdb-adapter-idb')) 6 | .plugin(require('pouchdb-adapter-http')) 7 | .plugin(require('pouchdb-mapreduce')) 8 | .plugin(require('pouchdb-replication')); -------------------------------------------------------------------------------- /lib/worker/safe-eval.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('debug')('pouchdb:worker'); 4 | 5 | module.exports = function safeEval(str) { 6 | log('safeEvaling', str); 7 | var target = {}; 8 | /* jshint evil: true */ 9 | eval('target.target = (' + str + ');'); 10 | log('returning', target.target); 11 | return target.target; 12 | }; -------------------------------------------------------------------------------- /lib/worker/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var safeEval = require('./safe-eval'); 4 | 5 | // mostly borrowed from express-pouchb's utils.sendError() 6 | exports.createError = function (err) { 7 | var status = err.status || 500; 8 | 9 | // last argument is optional 10 | if (err.name && err.message) { 11 | if (err.name === 'Error' || err.name === 'TypeError') { 12 | if (err.message.indexOf("Bad special document member") !== -1) { 13 | err.name = 'doc_validation'; 14 | // add more clauses here if the error name is too general 15 | } else { 16 | err.name = 'bad_request'; 17 | } 18 | } 19 | err = { 20 | error: err.name, 21 | name: err.name, 22 | reason: err.message, 23 | message: err.message, 24 | status: status 25 | }; 26 | } 27 | return err; 28 | }; 29 | 30 | exports.decodeArgs = function decodeArgs(args) { 31 | var funcArgs = ['filter', 'map', 'reduce']; 32 | args.forEach(function (arg) { 33 | if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) { 34 | funcArgs.forEach(function (funcArg) { 35 | if (!(funcArg in arg) || arg[funcArg] === null) { 36 | delete arg[funcArg]; 37 | } else if (arg[funcArg].type === 'func' && arg[funcArg].func) { 38 | arg[funcArg] = safeEval(arg[funcArg].func); 39 | } 40 | }); 41 | } 42 | }); 43 | return args; 44 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker-pouch", 3 | "version": "2.1.0", 4 | "description": "PouchDB over a web worker", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/nolanlawson/worker-pouch.git" 9 | }, 10 | "keywords": [ 11 | "pouch", 12 | "pouchdb", 13 | "plugin", 14 | "worker", 15 | "couch", 16 | "couchdb" 17 | ], 18 | "author": "", 19 | "license": "Apache-2.0", 20 | "bugs": { 21 | "url": "https://github.com/nolanlawson/worker-pouch/issues" 22 | }, 23 | "scripts": { 24 | "prepublish": "npm run build", 25 | "build-workerified-code": "node bin/build.js", 26 | "test-browser": "./bin/test-browser.js", 27 | "jshint": "jshint -c .jshintrc lib test/test.js", 28 | "test": "npm run jshint && npm run test-browser", 29 | "build": "mkdirp dist && run-scripts build-workerified-code browserify min", 30 | "browserify": "browserify -s workerPouch -p bundle-collapser/plugin . | ./bin/es3ify.js | derequire > dist/pouchdb.worker-pouch.js", 31 | "min": "UGLIFY=1 browserify -s workerPouch -p bundle-collapser/plugin . | ./bin/es3ify.js | derequire | uglifyjs -mc > dist/pouchdb.worker-pouch.min.js", 32 | "dev": "npm run build-workerified-code && browserify test/test.js > test/test-bundle.js && npm run dev-server", 33 | "dev-server": "./bin/dev-server.js", 34 | "build-custom-worker": "browserify test/custom-api/worker.js --debug > test/custom-api/worker-bundle.js", 35 | "test-custom-local": "run-scripts build-workerified-code build-custom-worker && zuul --ui mocha-bdd --local 9000 --no-coverage test/custom-api/test.js", 36 | "test-custom": "run-scripts build-workerified-code build-custom-worker && zuul --ui mocha-bdd --phantom --no-coverage test/custom-api/test.js" 37 | }, 38 | "dependencies": { 39 | "argsarray": "0.0.1", 40 | "blob-util": "^1.1.1", 41 | "debug": "^2.1.3", 42 | "inherits": "^2.0.1", 43 | "js-extend": "^1.0.1", 44 | "pouchdb-adapter-http": "6.2.0", 45 | "pouchdb-adapter-idb": "6.2.0", 46 | "pouchdb-binary-util": "^1.0.0", 47 | "pouchdb-core": "6.2.0", 48 | "pouchdb-mapreduce": "6.2.0", 49 | "pouchdb-promise": "6.2.0", 50 | "pouchdb-replication": "6.2.0" 51 | }, 52 | "devDependencies": { 53 | "bluebird": "^2.9.24", 54 | "browserify": "14.1.0", 55 | "bundle-collapser": "1.2.1", 56 | "chai": "^3.5.0", 57 | "chai-as-promised": "^5.3.0", 58 | "corsproxy": "^0.2.14", 59 | "derequire": "^2.0.0", 60 | "es3ify": "^0.1.3", 61 | "es5-shim": "^4.1.1", 62 | "http-server": "^0.8.5", 63 | "jshint": "2.8.0", 64 | "lodash.debounce": "4.0.8", 65 | "mkdirp": "^0.5.0", 66 | "mocha": "^2.3.3", 67 | "phantomjs-prebuilt": "^2.1.7", 68 | "pouchdb-browser": "6.2.0", 69 | "pouchdb-http-proxy": "^0.10.4", 70 | "pouchdb-memory": "6.0.0", 71 | "request": "^2.36.0", 72 | "run-scripts": "^0.4.0", 73 | "sauce-connect-launcher": "^0.11.1", 74 | "selenium-standalone": "6.4.1", 75 | "uglify-js": "^2.4.13", 76 | "watch-glob": "0.1.3", 77 | "wd": "^0.2.21", 78 | "zuul": "^3.10.1" 79 | }, 80 | "files": [ 81 | "lib", 82 | "dist", 83 | "client", 84 | "worker" 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /test/bind-polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | // minimal polyfill for phantomjs; in the future, we should do ES5_SHIM=true like pouchdb 4 | if (!Function.prototype.bind) { 5 | Function.prototype.bind = function (oThis) { 6 | if (typeof this !== "function") { 7 | // closest thing possible to the ECMAScript 5 8 | // internal IsCallable function 9 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 10 | } 11 | 12 | var aArgs = Array.prototype.slice.call(arguments, 1), 13 | fToBind = this, 14 | fNOP = function () {}, 15 | fBound = function () { 16 | return fToBind.apply(this instanceof fNOP && oThis 17 | ? this 18 | : oThis, 19 | aArgs.concat(Array.prototype.slice.call(arguments))); 20 | }; 21 | 22 | fNOP.prototype = this.prototype; 23 | fBound.prototype = new fNOP(); 24 | 25 | return fBound; 26 | }; 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /test/custom-api/service-worker-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | require('chai-as-promised'); 5 | var should = chai.should(); 6 | 7 | var PouchDB = require('pouchdb-browser'); 8 | PouchDB.adapter('worker', require('../../client')); 9 | 10 | describe('Service Worker custom api test suite', function () { 11 | 12 | var db; 13 | before(function () { 14 | return navigator.serviceWorker.register('sw.js', { 15 | scope: './' 16 | }).then(function () { 17 | if (navigator.serviceWorker.controller) { 18 | // already active and controlling this page 19 | return navigator.serviceWorker; 20 | } 21 | // wait for a new service worker to control this page 22 | return new Promise(function (resolve) { 23 | function onControllerChange() { 24 | navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange); 25 | resolve(navigator.serviceWorker); 26 | } 27 | navigator.serviceWorker.addEventListener('controllerchange', onControllerChange); 28 | }); 29 | }).then(function (serviceWorker) { // the worker is ready 30 | db = new PouchDB('testdb', { 31 | adapter: 'worker', 32 | worker: function() { 33 | return serviceWorker; 34 | } 35 | }); 36 | return db; 37 | }).catch(console.log.bind(console)); 38 | }); 39 | 40 | after(function () { 41 | return db.destroy(); 42 | }); 43 | 44 | it('should exist', function () { 45 | should.exist(db); 46 | }); 47 | 48 | it('should be valid', function () { 49 | db.adapter.should.equal('worker'); 50 | }); 51 | 52 | it('should put() and get data', function () { 53 | return db.put({ 54 | _id: 'foo' 55 | }).then(function () { 56 | return db.get('foo'); 57 | }).then(function (doc) { 58 | doc._id.should.equal('foo'); 59 | return db.info(); 60 | }).then(function (info) { 61 | info.doc_count.should.equal(1); 62 | }); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/custom-api/service-worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var registerWorkerPouch = require('../../worker'); 4 | // using in-memory so it will work in PhantomJS 5 | var PouchDB = require('pouchdb-memory'); 6 | var pouchCreator = function (opts) { 7 | opts.adapter = 'memory'; 8 | return new PouchDB(opts); 9 | }; 10 | registerWorkerPouch(self, pouchCreator); 11 | 12 | self.addEventListener('activate', function(event) { 13 | event.waitUntil(self.clients.claim()); // activate right now 14 | }); 15 | 16 | -------------------------------------------------------------------------------- /test/custom-api/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | require('chai-as-promised'); 5 | var should = chai.should(); 6 | 7 | var PouchDB = require('pouchdb-browser'); 8 | PouchDB.adapter('worker', require('../../client')); 9 | 10 | describe('custom api test suite', function () { 11 | 12 | this.timeout(180000); 13 | 14 | var db; 15 | 16 | before(function () { 17 | var worker = new Worker('/test/custom-api/worker-bundle.js'); 18 | db = new PouchDB('testdb', { 19 | adapter: 'worker', 20 | worker: function () { return worker; } 21 | }); 22 | }); 23 | 24 | after(function () { 25 | return db.destroy(); 26 | }); 27 | 28 | it('should exist', function () { 29 | should.exist(db); 30 | }); 31 | 32 | it('should be valid', function () { 33 | db.adapter.should.equal('worker'); 34 | }); 35 | 36 | it('should put() and get data', function () { 37 | return db.put({ 38 | _id: 'foo' 39 | }).then(function () { 40 | return db.get('foo'); 41 | }).then(function (doc) { 42 | doc._id.should.equal('foo'); 43 | return db.info(); 44 | }).then(function (info) { 45 | info.doc_count.should.equal(1); 46 | }); 47 | }); 48 | 49 | }); -------------------------------------------------------------------------------- /test/custom-api/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint worker:true */ 4 | 5 | var registerWorkerPouch = require('../../worker'); 6 | // using in-memory so it will work in PhantomJS 7 | var PouchDB = require('pouchdb-memory'); 8 | var pouchCreator = function (opts) { 9 | opts.adapter = 'memory'; 10 | return new PouchDB(opts); 11 | }; 12 | registerWorkerPouch(self, pouchCreator); -------------------------------------------------------------------------------- /test/index-suite1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |