Comparing version 0.0.10 to 0.0.11
@@ -13,2 +13,3 @@ [PouchDB](http://pouchdb.com/) - The Javascript Database that Syncs | ||
* Almost all Pull Requests for features or bug fixes will need tests (seriously, its really important) | ||
* Before opening a pull request run `$ grunt test` to lint test the changes and run node tests. Preferably run the browser tests as well. | ||
* Commit messages should follow the following style: | ||
@@ -38,3 +39,3 @@ | ||
$ cd pouchdb | ||
$ npm install -g grunt | ||
$ npm install -g grunt-cli | ||
$ npm install | ||
@@ -52,8 +53,15 @@ $ grunt | ||
Run all tests with: | ||
$ grunt node-qunit | ||
Run single test file `test.basics.js` with: | ||
$ grunt node-qunit --test=basics | ||
### Browser Tests | ||
$ grunt server cors-server forever | ||
$ grunt browser | ||
# Now visit http://127.0.0.1:8000/tests/test.html in your browser | ||
# add ?testFiles=test.basics.js to run single test file | ||
@@ -60,0 +68,0 @@ Git Essentials |
227
docs/api.md
@@ -12,29 +12,48 @@ --- | ||
* [Create a database](#create_a_database) | ||
* [Delete a database](#delete_a_database) | ||
* [Create a document](#create_a_document) | ||
* [Update a document](#update_a_document) | ||
* [Create a batch of documents](#create_a_batch_of_documents) | ||
* [Fetch a document](#fetch_a_document) | ||
* [Fetch documents](#fetch_documents) | ||
* [Query the database](#query_the_database) | ||
* [Delete a document](#delete_a_document) | ||
* [Get database information](#get_database_information) | ||
* [Listen to database changes](#listen_to_database_changes) | ||
* [Replicate a database](#replicate_a_database) | ||
* [Get document revision diffs](#document_revisions_diff) | ||
* [List all databases](#list_all_databases) | ||
* [Create a database](#create_a_database) | ||
* [Delete a database](#delete_a_database) | ||
* [Create a document](#create_a_document) | ||
* [Update a document](#update_a_document) | ||
* [Save an attachment](#save_an_attachment) | ||
* [Get an attachment](#get_an_attachment) | ||
* [Delete an attachment](#delete_an_attachment) | ||
* [Create a batch of documents](#create_a_batch_of_documents) | ||
* [Fetch a document](#fetch_a_document) | ||
* [Fetch documents](#fetch_documents) | ||
* [Query the database](#query_the_database) | ||
* [Delete a document](#delete_a_document) | ||
* [Get database information](#get_database_information) | ||
* [Listen to database changes](#listen_to_database_changes) | ||
* [Replicate a database](#replicate_a_database) | ||
* [Compact the database](#compact_the_database) | ||
* [Get document revision diffs](#document_revisions_diff) | ||
## Create a database | ||
## List all databases | ||
Pouch('idb://dbname', [options], [callback]) | ||
Pouch.allDbs(callback) | ||
This method gets an existing database if one exists or creates a new one if one does not exist. The protocol field denotes which backend you want to use (current options are `idb`, `http` and `leveldb`) | ||
Retrieves all databases from PouchDB. (Adapter prefix of database is included if it was created with a prefix.) | ||
var pouchdb; | ||
Pouch('idb://test', function(err, db) { | ||
pouchdb = db; | ||
// Use pouchdb to call further functions | ||
pouchdb.post(.... | ||
Pouch.allDbs(function(err, response) { | ||
// Response: | ||
// [ | ||
// "testdb", | ||
// "idb://testdb2" | ||
// ] | ||
}) | ||
## Create a database | ||
var pouchdb = Pouch('dbname', [options]) | ||
var pouchdb = Pouch('http://localhost:5984/dbname', [options]) | ||
This method gets an existing database if one exists or creates a new one if one does not exist. You may also explicitly specify which backend you want to use for local database (e.g. `idb://dbname` or `leveldb://dbname`) but usually it is convenient to let PouchDB choose the best backend by itself. | ||
Note: Pouch reserves the prefix '_pouch_' for the creation of local databases -- all local databases will automatically be preprended with '_pouch_'. | ||
var pouchdb = Pouch('test'); | ||
pouchdb.post(...; | ||
## Delete a database | ||
@@ -46,3 +65,3 @@ | ||
Pouch.destroy('idb://test', function(err, info) { | ||
Pouch.destroy('test', function(err, info) { | ||
// database deleted | ||
@@ -83,3 +102,3 @@ }) | ||
## Save an attachment to a document | ||
## Save an attachment | ||
@@ -91,3 +110,3 @@ db.putAttachment(id, rev, doc, type, [callback]) | ||
db.put({ _id: 'otherdoc', title: 'Legendary Hearts' }, function(err, response) { | ||
var doc = 'Legendary hearts, tear us all apart\nMake our emotions bleed, crying out in need'; | ||
var doc = new Blob(['Legendary hearts, tear us all apart\nMake our emotions bleed, crying out in need']); | ||
db.putAttachment('otherdoc/text', response.rev, doc, 'text/plain', function(err, res) { | ||
@@ -103,2 +122,60 @@ // Response: | ||
Within node you must use a Buffer: | ||
var doc = new Buffer('Legendary hearts, tear us all apart\nMake our emotions bleed, crying out in need'); | ||
### Save an inline attachment | ||
You can inline attachments inside the document. | ||
In this case the attachment data must be supplied as a base64 encoded string: | ||
{ | ||
"_id": "otherdoc", | ||
"title": "Legendary Hearts", | ||
"_attachments": { | ||
"text": { | ||
"content_type": "text/plain", | ||
"data": "TGVnZW5kYXJ5IGhlYXJ0cywgdGVhciB1cyBhbGwgYXBhcnQKTWFrZSBvdXIgZW1vdGlvbnMgYmxlZWQsIGNyeWluZyBvdXQgaW4gbmVlZA==" | ||
} | ||
} | ||
} | ||
See [Inline Attachments](http://wiki.apache.org/couchdb/HTTP_Document_API#Inline_Attachments) | ||
on the CouchDB Wiki. | ||
## Get an attachment | ||
db.getAttachment(id, [callback]) | ||
Get attachment data. | ||
db.getAttachment('otherdoc/text', function(err, res) { | ||
// Response: | ||
// Blob or Buffer | ||
}) | ||
In node you get Buffers and Blobs in the browser. | ||
### Inline attachments | ||
You can specify `attachments: true` in most get operations. | ||
The attachment data will then be included in the attachment stubs. | ||
## Delete an attachment | ||
db.removeAttachment(id, rev, [callback]) | ||
Delete an attachment from a doc. | ||
db.removeAttachment('otherdoc/text', '2-068E73F5B44FEC987B51354DFC772891', function(err, res) { | ||
// Response: | ||
// { | ||
// "ok": true, | ||
// "rev": "3-1F983211AB87EFCCC980974DFC27382F" | ||
// } | ||
}) | ||
## Create a batch of documents | ||
@@ -114,4 +191,6 @@ | ||
* `options.new_edits`: Prevent the database from assigning new revision IDs to the documents. | ||
* `options.new_edits`: Prevent the database from assigning new revision IDs to the documents. | ||
<span></span> | ||
db.bulkDocs({ docs: [{ title: 'Lisa Says' }] }, function(err, response) { | ||
@@ -134,14 +213,59 @@ // Response array: | ||
* `options.revs`: Include revision history of the document | ||
* `options.revs_info`: Include a list of revisions of the document, and their availability. | ||
* `options.rev`: Fetch specific revision of a document. Defaults to winning revision (see [couchdb guide](http://guide.couchdb.org/draft/conflicts.html). | ||
* `options.revs`: Include revision history of the document | ||
* `options.revs_info`: Include a list of revisions of the document, and their availability. | ||
* `options.open_revs`: Fetch all leaf revisions if open_revs="all" or fetch all leaf revisions specified in open_revs array. Leaves will be returned in the same order as specified in input array | ||
* `options.conflicts`: If specified conflicting leaf revisions will be attached in `_conflicts` array | ||
* `options.attachments`: Include attachment data | ||
<span></span> | ||
db.bulkDocs({ docs: [{ title: 'Lisa Says' }] }, function(err, response) { | ||
// Response array: | ||
db.get('mydoc', function(err, doc) { | ||
// Document: | ||
// { | ||
// "title": "Rock and Roll Heart", | ||
// "_id": "mydoc", | ||
// "_rev": "1-A6157A5EA545C99B00FF904EEF05FD9F" | ||
// } | ||
}) | ||
db.get("foo", {revs: true, revs_info: true}, function(err, res) { | ||
// Result: | ||
// { | ||
// "_id": "foo", | ||
// "_rev": "2-7051cbe5c8faecd085a3fa619e6e6337", | ||
// "_revisions": { | ||
// "start": 2, | ||
// "ids": [ | ||
// "7051cbe5c8faecd085a3fa619e6e6337", | ||
// "967a00dff5e02add41819138abb3284d" | ||
// ] | ||
// }, | ||
// "_revs_info": [ | ||
// { | ||
// "rev": "2-7051cbe5c8faecd085a3fa619e6e6337", | ||
// "status": "available" | ||
// }, | ||
// { | ||
// "rev": "1-967a00dff5e02add41819138abb3284d", | ||
// "status": "available" | ||
// } | ||
// ] | ||
// } | ||
}) | ||
db.get("foo", {open_revs: ["2-7051cbe5c8faecd085a3fa619e6e6337", "2-nonexistent"]}, function(err, res) { | ||
// Result: | ||
// [ | ||
// { | ||
// "ok": true, | ||
// "id": "828124B9-3973-4AF3-9DFD-A94CE4544005", | ||
// "rev": "1-A8BC08745E62E58830CA066D99E5F457" | ||
// "ok": { | ||
// "_id": "foo", | ||
// "_rev": "2-7051cbe5c8faecd085a3fa619e6e6337", | ||
// "value": "some document value" | ||
// } | ||
// }, | ||
// { | ||
// "missing": "2-nonexistent" | ||
// } | ||
@@ -151,11 +275,17 @@ // ] | ||
db.get('mydoc', function(err, doc) { | ||
// Document: | ||
db.get("foo", {conflicts: true}, function(err, res) { | ||
// Result: | ||
// { | ||
// "title": "Rock and Roll Heart", | ||
// "_id": "mydoc", | ||
// "_rev": "1-A6157A5EA545C99B00FF904EEF05FD9F" | ||
// "_id": "foo", | ||
// "_rev": "1-winning", | ||
// "value": "my document with conflicts", | ||
// "_conflicts": [ | ||
// "1-conflict1" | ||
// "1-conflict2" | ||
// ] | ||
// } | ||
}) | ||
## Fetch documents | ||
@@ -174,4 +304,5 @@ | ||
- the rows are returned in the same order as the supplied "keys" array | ||
- the row for a deleted document will have the revision ID of the deletion, and an extra key "deleted":true in the "value" property | ||
- the row for a deleted document will have the revision ID of the deletion, and an extra key "deleted":true in the "value" property | ||
- the row for a nonexistent document will just contain an "error" property with the value "not_found" | ||
* `options.attachments`: Include attachment data | ||
@@ -346,3 +477,3 @@ <span></span> | ||
* `options.continuous`: Use _longpoll_ feed | ||
* `options.onChange`: Function called on each change after deduplication (only sends the most recent for each document), not called as a callback but called as onChange(change). Use with `continuous` flag. If you want to | ||
* `options.onChange`: Function called on each change after deduplication (only sends the most recent for each document), not called as a callback but called as onChange(change). Use with `continuous` flag. If you want to | ||
@@ -424,12 +555,20 @@ <span></span> | ||
* `options.filter`: Reference a filter function from a design document to selectively get updates | ||
* `options.complete`: Function called when all changes have been processed, defaults to the callback | ||
* `options.onChange`: Function called on each change after deduplication (only sends the most recent for each document), not called as a callback but called as onChange(change) | ||
* `options.filter`: Reference a filter function from a design document to selectively get updates | ||
* `options.complete`: Function called when all changes have been processed, defaults to the callback | ||
* `options.onChange`: Function called on each change after deduplication (only sends the most recent for each document), not called as a callback but called as onChange(change) | ||
<span></span> | ||
Pouch.replicate('idb://mydb', 'http://localhost:5984/mydb', function(err, changes) { | ||
Pouch.replicate('mydb', 'http://localhost:5984/mydb', function(err, changes) { | ||
// | ||
}) | ||
## Compact the database | ||
db.compact([opts], [callback]) | ||
Runs compaction of the database. Fires callback when compaction is done. If you use http adapter and have specified callback Pouch will ping the remote database in regular intervals unless the compaction is finished. | ||
* `options.interval`: Number of milliseconds Pouch waits before asking again if compaction is already done. Only for http adapter. | ||
## Document Revisions Diff | ||
@@ -439,3 +578,3 @@ | ||
Given a set of document/revision IDs, returns the subset of those that do not correspond | ||
Given a set of document/revision IDs, returns the subset of those that do not correspond | ||
to revisions stored in the database. Primarily used in replication. | ||
@@ -442,0 +581,0 @@ |
@@ -6,7 +6,15 @@ { | ||
"author": "Dale Harvey", | ||
"version": "0.0.10", | ||
"version": "0.0.11", | ||
"main": "./src/pouch.js", | ||
"homepage": "https://github.com/daleharvey/pouchdb", | ||
"keywords": [ "db", "couchdb", "pouchdb" ], | ||
"tags": [ "db", "couchdb", "pouchdb" ], | ||
"keywords": [ | ||
"db", | ||
"couchdb", | ||
"pouchdb" | ||
], | ||
"tags": [ | ||
"db", | ||
"couchdb", | ||
"pouchdb" | ||
], | ||
"dependencies": { | ||
@@ -19,3 +27,7 @@ "levelup": "*", | ||
"main": "dist/pouch.amd-nightly.js", | ||
"include": ["dist/pouch.amd-nightly.js", "README.md", "LICENSE"], | ||
"include": [ | ||
"dist/pouch.amd-nightly.js", | ||
"README.md", | ||
"LICENSE" | ||
], | ||
"dependencies": { | ||
@@ -33,7 +45,13 @@ "simple-uuid": null, | ||
"send": "*", | ||
"grunt": "0.3", | ||
"grunt-saucelabs": "*", | ||
"grunt-node-qunit": "*", | ||
"grunt": "~0.4", | ||
"grunt-saucelabs": "~3.0", | ||
"grunt-node-qunit": "~2.0", | ||
"corsproxy": "*", | ||
"http-proxy": "*" | ||
"http-proxy": "*", | ||
"grunt-contrib-connect": "~0.1.2", | ||
"grunt-contrib-concat": "~0.1.3", | ||
"grunt-contrib-uglify": "~0.1.1", | ||
"grunt-contrib-jshint": "~0.1.1", | ||
"grunt-contrib-clean": "~0.4.0", | ||
"grunt-contrib-watch": "~0.2.0" | ||
}, | ||
@@ -43,7 +61,7 @@ "maintainers": [ | ||
"name": "Dale Harvey", | ||
"web" : "https://github.com/daleharvey" | ||
"web": "https://github.com/daleharvey" | ||
}, | ||
{ | ||
"name":"Ryan Ramage", | ||
"web":"https://github.com/ryanramage" | ||
"name": "Ryan Ramage", | ||
"web": "https://github.com/ryanramage" | ||
} | ||
@@ -53,9 +71,9 @@ ], | ||
{ | ||
"type":"git", | ||
"url":"https://github.com/daleharvey/pouchdb" | ||
"type": "git", | ||
"url": "https://github.com/daleharvey/pouchdb" | ||
} | ||
], | ||
"scripts": { | ||
"test": "grunt test" | ||
"test": "grunt test-travis" | ||
} | ||
} |
@@ -201,2 +201,6 @@ /*globals Pouch: true, call: false, ajax: true */ | ||
api.request = function(options, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('request', arguments); | ||
return; | ||
} | ||
options.auth = host.auth; | ||
@@ -209,3 +213,11 @@ options.url = genDBUrl(host, options.url); | ||
// version: The version of CouchDB it is running | ||
api.compact = function(callback) { | ||
api.compact = function(opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('compact', arguments); | ||
return; | ||
} | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
ajax({ | ||
@@ -215,3 +227,17 @@ auth: host.auth, | ||
method: 'POST' | ||
}, callback); | ||
}, function() { | ||
function ping() { | ||
api.info(function(err, res) { | ||
if (!res.compact_running) { | ||
call(callback, null); | ||
} else { | ||
setTimeout(ping, opts.interval || 200); | ||
} | ||
}); | ||
} | ||
// Ping the http if it's finished compaction | ||
if (typeof callback === "function") { | ||
ping(); | ||
} | ||
}); | ||
}; | ||
@@ -223,2 +249,6 @@ | ||
api.info = function(callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('info', arguments); | ||
return; | ||
} | ||
ajax({ | ||
@@ -235,2 +265,6 @@ auth: host.auth, | ||
api.get = function(id, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('get', arguments); | ||
return; | ||
} | ||
// If no options were given, set the callback to the second parameter | ||
@@ -312,4 +346,4 @@ if (typeof opts === 'function') { | ||
(parts.length > 2 && parts[0] === '_design' && parts[0] !== '_local')) { | ||
// Nothing is expected back from the server | ||
options.json = false; | ||
// Binary is expected back from the server | ||
options.binary = true; | ||
} | ||
@@ -321,3 +355,3 @@ | ||
if (err) { | ||
return call(callback, Pouch.Errors.MISSING_DOC); | ||
return call(callback, err); | ||
} | ||
@@ -332,2 +366,6 @@ | ||
api.remove = function(doc, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('remove', arguments); | ||
return; | ||
} | ||
// If no options were given, set the callback to be the second parameter | ||
@@ -349,2 +387,6 @@ if (typeof opts === 'function') { | ||
api.removeAttachment = function idb_removeAttachment(id, rev, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('removeAttachment', arguments); | ||
return; | ||
} | ||
ajax({ | ||
@@ -357,6 +399,26 @@ auth: host.auth, | ||
// Add the attachment given by doc and the content type given by type | ||
// Add the attachment given by blob and its contentType property | ||
// to the document with the given id, the revision given by rev, and | ||
// add it to the database given by host. | ||
api.putAttachment = function(id, rev, doc, type, callback) { | ||
api.putAttachment = function(id, rev, blob, type, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('putAttachment', arguments); | ||
return; | ||
} | ||
if (typeof type === 'function') { | ||
callback = type; | ||
type = blob; | ||
blob = rev; | ||
rev = null; | ||
} | ||
if (typeof type === 'undefined') { | ||
type = blob; | ||
blob = rev; | ||
rev = null; | ||
} | ||
var url = genDBUrl(host, id); | ||
if (rev) { | ||
url += '?rev=' + rev; | ||
} | ||
// Add the attachment | ||
@@ -366,5 +428,6 @@ ajax({ | ||
method:'PUT', | ||
url: genDBUrl(host, id) + '?rev=' + rev, | ||
url: url, | ||
headers: {'Content-Type': type}, | ||
body: doc | ||
processData: false, | ||
body: blob | ||
}, callback); | ||
@@ -376,2 +439,6 @@ }; | ||
api.put = function(doc, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('put', arguments); | ||
return; | ||
} | ||
// If no options were given, set the callback to be the second parameter | ||
@@ -416,2 +483,6 @@ if (typeof opts === 'function') { | ||
api.post = function(doc, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('post', arguments); | ||
return; | ||
} | ||
// If no options were given, set the callback to be the second parameter | ||
@@ -443,2 +514,6 @@ if (typeof opts === 'function') { | ||
api.bulkDocs = function(req, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('bulkDocs', arguments); | ||
return; | ||
} | ||
// If no options were given, set the callback to be the second parameter | ||
@@ -471,2 +546,8 @@ if (typeof opts === 'function') { | ||
api._getAttachment = function (id, opts, callback) { | ||
api.get(id.docId + "/" + id.attachmentId, function(err, res) { | ||
callback(err, res); | ||
}); | ||
}; | ||
// Get a listing of the documents in the database given | ||
@@ -476,2 +557,6 @@ // by host and ordered by increasing id. | ||
// If no options were given, set the callback to be the second parameter | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('allDocs', arguments); | ||
return; | ||
} | ||
if (typeof opts === 'function') { | ||
@@ -522,2 +607,7 @@ callback = opts; | ||
// If opts.limit exists, add the limit value to the parameter list. | ||
if (opts.limit) { | ||
params.push('limit=' + opts.limit); | ||
} | ||
// Format the list of parameters into a valid URI query string | ||
@@ -548,2 +638,6 @@ params = params.join('&'); | ||
api.changes = function(opts) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('changes', arguments); | ||
return; | ||
} | ||
@@ -584,2 +678,6 @@ if (Pouch.DEBUG) { | ||
if (opts.limit || opts.limit === 0) { | ||
params.push('limit=' + opts.limit); | ||
} | ||
// If opts.descending exists, add the descending value to the query string. | ||
@@ -650,6 +748,11 @@ // if descending=true then the change results are returned in | ||
var hasFilter = opts.filter && typeof opts.filter === 'function'; | ||
if (opts.aborted || hasFilter && !opts.filter.apply(this, [c.doc])) { | ||
var req = {}; | ||
req.query = opts.query_params; | ||
if (opts.aborted || hasFilter && !opts.filter.apply(this, [c.doc, req])) { | ||
return; | ||
} | ||
if (opts.doc_ids && opts.doc_ids.indexOf(c.id) !== -1) { | ||
return; | ||
} | ||
// Process the change | ||
@@ -709,2 +812,6 @@ call(opts.onChange, c); | ||
api.revsDiff = function(req, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('revsDiff', arguments); | ||
return; | ||
} | ||
// If no options were given, set the callback to be the second parameter | ||
@@ -723,3 +830,3 @@ if (typeof opts === 'function') { | ||
}, function(err, res) { | ||
call(callback, null, res); | ||
call(callback, err, res); | ||
}); | ||
@@ -729,2 +836,6 @@ }; | ||
api.close = function(callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('close', arguments); | ||
return; | ||
} | ||
call(callback, null); | ||
@@ -731,0 +842,0 @@ }; |
@@ -0,5 +1,10 @@ | ||
/*globals call: false, extend: false, parseDoc: false, Crypto: false */ | ||
/*globals isLocalId: false, isDeleted: false, Changes: false, filterChange: false */ | ||
'use strict'; | ||
// While most of the IDB behaviors match between implementations a | ||
// lot of the names still differ. This section tries to normalize the | ||
// different objects & methods. | ||
window.indexedDB = window.indexedDB || | ||
var indexedDB = window.indexedDB || | ||
window.mozIndexedDB || | ||
@@ -12,9 +17,9 @@ window.webkitIndexedDB; | ||
// on the native IDBTransaction object | ||
window.IDBTransaction = (window.IDBTransaction && window.IDBTransaction.READ_WRITE) | ||
? window.IDBTransaction | ||
: (window.webkitIDBTransaction && window.webkitIDBTransaction.READ_WRITE) | ||
? window.webkitIDBTransaction | ||
: { READ_WRITE: 'readwrite' }; | ||
var IDBTransaction = (window.IDBTransaction && window.IDBTransaction.READ_WRITE) ? | ||
window.IDBTransaction : | ||
(window.webkitIDBTransaction && window.webkitIDBTransaction.READ_WRITE) ? | ||
window.webkitIDBTransaction : | ||
{ READ_WRITE: 'readwrite' }; | ||
window.IDBKeyRange = window.IDBKeyRange || | ||
var IDBKeyRange = window.IDBKeyRange || | ||
window.webkitIDBKeyRange; | ||
@@ -55,2 +60,4 @@ | ||
var META_STORE = 'meta-store'; | ||
// Where we detect blob support | ||
var DETECT_BLOB_SUPPORT_STORE = 'detect-blob-support'; | ||
@@ -62,5 +69,7 @@ | ||
id: 'meta-store', | ||
updateSeq: 0, | ||
updateSeq: 0 | ||
}; | ||
var blobSupport = null; | ||
var instanceId = null; | ||
@@ -70,16 +79,26 @@ var api = {}; | ||
if (Pouch.DEBUG) | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Open Database'); | ||
} | ||
// TODO: before we release, make sure we write upgrade needed | ||
// in a way that supports a future upgrade path | ||
req.onupgradeneeded = function(e) { | ||
var db = e.target.result; | ||
var currentVersion = e.oldVersion; | ||
while (currentVersion !== e.newVersion) { | ||
if (currentVersion === 0) { | ||
createSchema(db); | ||
} | ||
currentVersion++; | ||
} | ||
}; | ||
function createSchema(db) { | ||
db.createObjectStore(DOC_STORE, {keyPath : 'id'}) | ||
.createIndex('seq', 'seq', {unique: true}); | ||
db.createObjectStore(BY_SEQ_STORE, {autoIncrement : true}) | ||
.createIndex('_rev', '_rev', {unique: true}); | ||
.createIndex('_doc_id_rev', '_doc_id_rev', {unique: true}); | ||
db.createObjectStore(ATTACH_STORE, {keyPath: 'digest'}); | ||
db.createObjectStore(META_STORE, {keyPath: 'id', autoIncrement: false}); | ||
}; | ||
db.createObjectStore(DETECT_BLOB_SUPPORT_STORE); | ||
} | ||
@@ -90,3 +109,4 @@ req.onsuccess = function(e) { | ||
var txn = idb.transaction([META_STORE], IDBTransaction.READ_WRITE); | ||
var txn = idb.transaction([META_STORE, DETECT_BLOB_SUPPORT_STORE], | ||
IDBTransaction.READ_WRITE); | ||
@@ -129,4 +149,13 @@ idb.onversionchange = function() { | ||
} | ||
call(callback, null, api); | ||
} | ||
// detect blob support | ||
try { | ||
txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(new Blob(), "key"); | ||
blobSupport = true; | ||
} catch (err) { | ||
blobSupport = false; | ||
} finally { | ||
call(callback, null, api); | ||
} | ||
}; | ||
}; | ||
@@ -146,19 +175,6 @@ | ||
api.bulkDocs = function idb_bulkDocs(req, opts, callback) { | ||
api._bulkDocs = function idb_bulkDocs(req, opts, callback) { | ||
var newEdits = opts.new_edits; | ||
var userDocs = req.docs; | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
if (!req.docs) { | ||
return call(callback, Pouch.Errors.MISSING_BULK_DOCS); | ||
} | ||
var newEdits = 'new_edits' in opts ? opts.new_edits : true; | ||
var userDocs = extend(true, [], req.docs); | ||
// Parse the docs, give them a sequence number for the result | ||
@@ -168,8 +184,2 @@ var docInfos = userDocs.map(function(doc, i) { | ||
newDoc._bulk_seq = i; | ||
if (doc._deleted) { | ||
if (!newDoc.metadata.deletions) { | ||
newDoc.metadata.deletions = {}; | ||
} | ||
newDoc.metadata.deletions[doc._rev.split('-')[1]] = true; | ||
} | ||
return newDoc; | ||
@@ -193,3 +203,3 @@ }); | ||
} | ||
if (!docs.length || docInfo.metadata.id !== docs[0].metadata.id) { | ||
if (!docs.length || !newEdits || docInfo.metadata.id !== docs[0].metadata.id) { | ||
return docs.unshift(docInfo); | ||
@@ -240,3 +250,3 @@ } | ||
IdbPouch.Changes.notify(name); | ||
localStorage[name] = (localStorage[name] === "a") ? "b" : "a"; | ||
IdbPouch.Changes.notifyLocalWindows(name); | ||
}); | ||
@@ -246,2 +256,66 @@ call(callback, null, aresults); | ||
function preprocessAttachment(att, finish) { | ||
if (att.stub) { | ||
return finish(); | ||
} | ||
if (typeof att.data === 'string') { | ||
var data; | ||
try { | ||
data = atob(att.data); | ||
} catch(e) { | ||
return call(callback, Pouch.error(Pouch.Errors.BAD_ARG, "Attachments need to be base64 encoded")); | ||
} | ||
att.digest = 'md5-' + Crypto.MD5(data); | ||
if (blobSupport) { | ||
var type = att.content_type; | ||
att.data = new Blob([data], {type: type}); | ||
} | ||
return finish(); | ||
} | ||
var reader = new FileReader(); | ||
reader.onloadend = function(e) { | ||
att.digest = 'md5-' + Crypto.MD5(this.result); | ||
if (!blobSupport) { | ||
att.data = btoa(this.result); | ||
} | ||
finish(); | ||
}; | ||
reader.readAsBinaryString(att.data); | ||
} | ||
function preprocessAttachments(callback) { | ||
if (!docInfos.length) { | ||
return callback(); | ||
} | ||
var docv = 0; | ||
docInfos.forEach(function(docInfo) { | ||
var attachments = docInfo.data && docInfo.data._attachments ? | ||
Object.keys(docInfo.data._attachments) : []; | ||
if (!attachments.length) { | ||
return done(); | ||
} | ||
var recv = 0; | ||
function attachmentProcessed() { | ||
recv++; | ||
if (recv === attachments.length) { | ||
done(); | ||
} | ||
} | ||
for (var key in docInfo.data._attachments) { | ||
preprocessAttachment(docInfo.data._attachments[key], attachmentProcessed); | ||
} | ||
}); | ||
function done() { | ||
docv++; | ||
if (docInfos.length === docv) { | ||
callback(); | ||
} | ||
} | ||
} | ||
function writeDoc(docInfo, callback) { | ||
@@ -264,12 +338,24 @@ var err = null; | ||
function collectResults(attachmentErr) { | ||
if (!err) { | ||
if (attachmentErr) { | ||
err = attachmentErr; | ||
call(callback, err); | ||
} else if (recv === attachments.length) { | ||
finish(); | ||
} | ||
} | ||
} | ||
function attachmentSaved(err) { | ||
recv++; | ||
collectResults(err); | ||
} | ||
for (var key in docInfo.data._attachments) { | ||
if (!docInfo.data._attachments[key].stub) { | ||
var data = docInfo.data._attachments[key].data; | ||
var digest = 'md5-' + Crypto.MD5(data); | ||
delete docInfo.data._attachments[key].data; | ||
docInfo.data._attachments[key].digest = digest; | ||
saveAttachment(docInfo, digest, data, function(err) { | ||
recv++; | ||
collectResults(err); | ||
}); | ||
var digest = docInfo.data._attachments[key].digest; | ||
saveAttachment(docInfo, digest, data, attachmentSaved); | ||
} else { | ||
@@ -281,22 +367,9 @@ recv++; | ||
if (!attachments.length) { | ||
finish(); | ||
} | ||
function collectResults(attachmentErr) { | ||
if (!err) { | ||
if (attachmentErr) { | ||
err = attachmentErr; | ||
call(callback, err); | ||
} else if (recv == attachments.length) { | ||
finish(); | ||
} | ||
} | ||
} | ||
function finish() { | ||
docInfo.data._doc_id_rev = docInfo.data._id + "::" + docInfo.data._rev; | ||
var dataReq = txn.objectStore(BY_SEQ_STORE).put(docInfo.data); | ||
dataReq.onsuccess = function(e) { | ||
if (Pouch.DEBUG) | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Wrote Document ', docInfo.metadata.id); | ||
} | ||
docInfo.metadata.seq = e.target.result; | ||
@@ -312,12 +385,14 @@ // Current _rev is calculated from _rev_tree on read | ||
} | ||
if (!attachments.length) { | ||
finish(); | ||
} | ||
} | ||
function updateDoc(oldDoc, docInfo) { | ||
docInfo.metadata.deletions = extend(docInfo.metadata.deletions, oldDoc.deletions); | ||
var merged = Pouch.merge(oldDoc.rev_tree, docInfo.metadata.rev_tree[0], 1000); | ||
var wasPreviouslyDeleted = isDeleted(oldDoc); | ||
var inConflict = (wasPreviouslyDeleted && isDeleted(docInfo.metadata)) || | ||
(!wasPreviouslyDeleted && newEdits && merged.conflicts !== 'new_leaf'); | ||
var inConflict = (isDeleted(oldDoc) && isDeleted(docInfo.metadata)) || | ||
(!isDeleted(oldDoc) && newEdits && merged.conflicts !== 'new_leaf'); | ||
if (inConflict) { | ||
@@ -350,33 +425,26 @@ results.push(makeErr(Pouch.Errors.REV_CONFLICT, docInfo._bulk_seq)); | ||
var getReq = objectStore.get(digest).onsuccess = function(e) { | ||
var originalRefs = e.target.result && e.target.result.refs || {}; | ||
var ref = [docInfo.metadata.id, docInfo.metadata.rev].join('@'); | ||
var newAtt = {digest: digest, body: data}; | ||
if (e.target.result) { | ||
if (e.target.result.refs) { | ||
// only update references if this attachment already has them | ||
// since we cannot migrate old style attachments here without | ||
// doing a full db scan for references | ||
newAtt.refs = e.target.result.refs; | ||
newAtt.refs[ref] = true; | ||
} | ||
} else { | ||
newAtt.refs = {}; | ||
newAtt.refs[ref] = true; | ||
} | ||
var newAtt = { | ||
digest: digest, | ||
body: data, | ||
refs: originalRefs | ||
}; | ||
newAtt.refs[ref] = true; | ||
var putReq = objectStore.put(newAtt).onsuccess = function(e) { | ||
call(callback); | ||
}; | ||
putReq.onerror = putReq.ontimeout = idbError(callback); | ||
}; | ||
getReq.onerror = getReq.ontimeout = idbError(callback); | ||
} | ||
var txn = idb.transaction([DOC_STORE, BY_SEQ_STORE, ATTACH_STORE, META_STORE], IDBTransaction.READ_WRITE); | ||
txn.onerror = idbError(callback); | ||
txn.ontimeout = idbError(callback); | ||
txn.oncomplete = complete; | ||
var txn; | ||
preprocessAttachments(function() { | ||
txn = idb.transaction([DOC_STORE, BY_SEQ_STORE, ATTACH_STORE, META_STORE], | ||
IDBTransaction.READ_WRITE); | ||
txn.onerror = idbError(callback); | ||
txn.ontimeout = idbError(callback); | ||
txn.oncomplete = complete; | ||
processDocs(); | ||
processDocs(); | ||
}); | ||
}; | ||
@@ -390,74 +458,35 @@ | ||
// current revision(s) from the by sequence store | ||
api.get = function idb_get(id, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
id = parseDocId(id); | ||
if (id.attachmentId !== '') { | ||
return api.getAttachment(id, {decode: true}, callback); | ||
} | ||
api._get = function idb_get(id, opts, callback) { | ||
var result; | ||
var metadata; | ||
var txn = idb.transaction([DOC_STORE, BY_SEQ_STORE, ATTACH_STORE], 'readonly'); | ||
txn.oncomplete = function() { | ||
// Leaves are set when we ask about open_revs | ||
// Using this approach they can be quite easily abstracted out to some | ||
// generic api.get | ||
if (leaves) { | ||
result = []; | ||
var count = leaves.length; | ||
leaves.forEach(function(leaf){ | ||
api.get(id.docId, {rev: leaf}, function(err, doc){ | ||
if (!err) { | ||
result.push({ok: doc}); | ||
} else { | ||
result.push({missing: leaf}); | ||
} | ||
count--; | ||
if(!count) { | ||
finish(); | ||
} | ||
}); | ||
}); | ||
} else { | ||
finish(); | ||
} | ||
call(callback, result, metadata); | ||
}; | ||
function finish() { | ||
if ('error' in result) { | ||
call(callback, result); | ||
} else { | ||
call(callback, null, result); | ||
} | ||
} | ||
var leaves; | ||
txn.objectStore(DOC_STORE).get(id.docId).onsuccess = function(e) { | ||
var metadata = e.target.result; | ||
if (!e.target.result || (isDeleted(metadata, opts.rev) && !opts.rev)) { | ||
metadata = e.target.result; | ||
// we can determine the result here if: | ||
// 1. there is no such document | ||
// 2. the document is deleted and we don't ask about specific rev | ||
// When we ask with opts.rev we expect the answer to be either | ||
// doc (possibly with _deleted=true) or missing error | ||
if (!metadata) { | ||
result = Pouch.Errors.MISSING_DOC; | ||
return; | ||
} | ||
if (opts.open_revs) { | ||
if (opts.open_revs === "all") { | ||
leaves = collectLeaves(metadata.rev_tree).map(function(leaf){ | ||
return leaf.rev; | ||
}); | ||
} else { | ||
leaves = opts.open_revs; // should be some validation here | ||
} | ||
return; // open_revs can be used only with revs | ||
if (isDeleted(metadata) && !opts.rev) { | ||
result = Pouch.error(Pouch.Errors.MISSING_DOC, "deleted"); | ||
return; | ||
} | ||
var rev = Pouch.merge.winningRev(metadata); | ||
var key = opts.rev ? opts.rev : rev; | ||
var index = txn.objectStore(BY_SEQ_STORE).index('_rev'); | ||
var key = metadata.id + '::' + (opts.rev ? opts.rev : rev); | ||
var index = txn.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); | ||
index.get(key).onsuccess = function(e) { | ||
var doc = e.target.result; | ||
if(doc && doc._doc_id_rev) { | ||
delete(doc._doc_id_rev); | ||
} | ||
if (!doc) { | ||
@@ -467,23 +496,2 @@ result = Pouch.Errors.MISSING_DOC; | ||
} | ||
if (opts.revs) { // FIXME: if rev is given it should return ids from root to rev (don't include newer) | ||
var path = arrayFirst(rootToLeaf(metadata.rev_tree), function(arr) { | ||
return arr.ids.indexOf(doc._rev.split('-')[1]) !== -1; | ||
}); | ||
path.ids.reverse(); | ||
doc._revisions = { | ||
start: (path.pos + path.ids.length) - 1, | ||
ids: path.ids | ||
}; | ||
} | ||
if (opts.revs_info) { // FIXME: this returns revs for whole tree and should return only branch for winner | ||
doc._revs_info = metadata.rev_tree.reduce(function(prev, current) { | ||
return prev.concat(collectRevs(current)); | ||
}, []); | ||
} | ||
if (opts.conflicts) { | ||
var conflicts = collectConflicts(metadata.rev_tree, metadata.deletions); | ||
if (conflicts.length) { | ||
doc._conflicts = conflicts; | ||
} | ||
} | ||
if (opts.attachments && doc._attachments) { | ||
@@ -494,3 +502,3 @@ var attachments = Object.keys(doc._attachments); | ||
attachments.forEach(function(key) { | ||
api.getAttachment(doc._id + '/' + key, {txn: txn}, function(err, data) { | ||
api.getAttachment(doc._id + '/' + key, {encode: true, txn: txn}, function(err, data) { | ||
doc._attachments[key].data = data; | ||
@@ -515,11 +523,3 @@ | ||
api.getAttachment = function(id, opts, callback) { | ||
if (opts instanceof Function) { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (typeof id === 'string') { | ||
id = parseDocId(id); | ||
} | ||
api._getAttachment = function(id, opts, callback) { | ||
var result; | ||
@@ -534,3 +534,3 @@ var txn; | ||
txn = idb.transaction([DOC_STORE, BY_SEQ_STORE, ATTACH_STORE], 'readonly'); | ||
txn.oncomplete = function() { call(callback, null, result); } | ||
txn.oncomplete = function() { call(callback, null, result); }; | ||
} | ||
@@ -544,43 +544,41 @@ | ||
var digest = attachment.digest; | ||
var type = attachment.content_type | ||
var type = attachment.content_type; | ||
function postProcessDoc(data) { | ||
if (opts.decode) { | ||
data = atob(data); | ||
} | ||
return data; | ||
} | ||
txn.objectStore(ATTACH_STORE).get(digest).onsuccess = function(e) { | ||
var data = e.target.result.body; | ||
result = postProcessDoc(data); | ||
if ('txn' in opts) { | ||
call(callback, null, result); | ||
if (opts.encode) { | ||
if (blobSupport) { | ||
var reader = new FileReader(); | ||
reader.onloadend = function(e) { | ||
result = btoa(this.result); | ||
if ('txn' in opts) { | ||
call(callback, null, result); | ||
} | ||
}; | ||
reader.readAsBinaryString(data); | ||
} else { | ||
result = data; | ||
if ('txn' in opts) { | ||
call(callback, null, result); | ||
} | ||
} | ||
} else { | ||
if (blobSupport) { | ||
result = data; | ||
} else { | ||
result = new Blob([atob(data)], {type: type}); | ||
} | ||
if ('txn' in opts) { | ||
call(callback, null, result); | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
} | ||
}; | ||
return; | ||
} | ||
}; | ||
api.allDocs = function idb_allDocs(opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if ('keys' in opts) { | ||
if ('startkey' in opts) { | ||
call(callback, extend({ | ||
reason: 'Query parameter `start_key` is not compatible with multi-get' | ||
}, Pouch.Errors.QUERY_PARSE_ERROR)); | ||
return; | ||
} | ||
if ('endkey' in opts) { | ||
call(callback, extend({ | ||
reason: 'Query parameter `end_key` is not compatible with multi-get' | ||
}, Pouch.Errors.QUERY_PARSE_ERROR)); | ||
return; | ||
} | ||
} | ||
api._allDocs = function idb_allDocs(opts, callback) { | ||
var start = 'startkey' in opts ? opts.startkey : false; | ||
@@ -612,3 +610,3 @@ var end = 'endkey' in opts ? opts.endkey : false; | ||
total_rows: results.length, | ||
rows: results | ||
rows: ('limit' in opts) ? results.slice(0, opts.limit) : results | ||
}); | ||
@@ -627,2 +625,3 @@ }; | ||
var cursor = e.target.result; | ||
var metadata = cursor.value; | ||
// If opts.keys is set we want to filter here only those docs with | ||
@@ -645,4 +644,8 @@ // key in opts.keys. With no performance tests it is difficult to | ||
doc.doc._rev = Pouch.merge.winningRev(metadata); | ||
if (doc.doc._doc_id_rev) { | ||
delete(doc.doc._doc_id_rev); | ||
} | ||
if (opts.conflicts) { | ||
doc.doc._conflicts = collectConflicts(metadata.rev_tree, metadata.deletions); | ||
doc.doc._conflicts = Pouch.merge.collectConflicts(metadata) | ||
.map(function(x) { return x.id; }); | ||
} | ||
@@ -667,10 +670,12 @@ } | ||
if (!opts.include_docs) { | ||
allDocsInner(cursor.value); | ||
allDocsInner(metadata); | ||
} else { | ||
var index = transaction.objectStore(BY_SEQ_STORE); | ||
index.get(cursor.value.seq).onsuccess = function(event) { | ||
var index = transaction.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); | ||
var mainRev = Pouch.merge.winningRev(metadata); | ||
var key = metadata.id + "::" + mainRev; | ||
index.get(key).onsuccess = function(event) { | ||
allDocsInner(cursor.value, event.target.result); | ||
}; | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -680,3 +685,3 @@ | ||
// easiest to implement though, should probably keep a counter | ||
api.info = function idb_info(callback) { | ||
api._info = function idb_info(callback) { | ||
var count = 0; | ||
@@ -707,7 +712,27 @@ var result; | ||
api.changes = function idb_changes(opts) { | ||
if (Pouch.DEBUG) | ||
api._changes = function idb_changes(opts) { | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Start Changes Feed: continuous=' + opts.continuous); | ||
} | ||
if (!opts.since) { | ||
opts.since = 0; | ||
} | ||
if (opts.continuous) { | ||
var id = name + ':' + Math.uuid(); | ||
opts.cancelled = false; | ||
IdbPouch.Changes.addListener(name, id, api, opts); | ||
IdbPouch.Changes.notify(name); | ||
return { | ||
cancel: function() { | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Cancel Changes Feed'); | ||
} | ||
opts.cancelled = true; | ||
IdbPouch.Changes.removeListener(name, id); | ||
} | ||
}; | ||
} | ||
var descending = 'descending' in opts ? opts.descending : false; | ||
@@ -720,8 +745,32 @@ descending = descending ? 'prev' : null; | ||
var results = [], resultIndices = {}, dedupResults = []; | ||
var id = name + ':' + Math.uuid(); | ||
var txn; | ||
function fetchChanges() { | ||
txn = idb.transaction([DOC_STORE, BY_SEQ_STORE]); | ||
txn.oncomplete = onTxnComplete; | ||
var req; | ||
if (opts.limit && descending) { | ||
req = txn.objectStore(BY_SEQ_STORE) | ||
.openCursor(IDBKeyRange.bound(opts.since, opts.since + opts.limit, true), descending); | ||
} else if (opts.limit && !descending) { | ||
req = txn.objectStore(BY_SEQ_STORE) | ||
.openCursor(IDBKeyRange.bound(opts.since, opts.since + opts.limit, true)); | ||
} else if (descending) { | ||
req = txn.objectStore(BY_SEQ_STORE) | ||
.openCursor(IDBKeyRange.lowerBound(opts.since, true), descending); | ||
} else { | ||
req = txn.objectStore(BY_SEQ_STORE) | ||
.openCursor(IDBKeyRange.lowerBound(opts.since, true)); | ||
} | ||
req.onsuccess = onsuccess; | ||
req.onerror = onerror; | ||
} | ||
if (opts.filter && typeof opts.filter === 'string') { | ||
var filterName = opts.filter.split('/'); | ||
api.get('_design/' + filterName[0], function(err, ddoc) { | ||
/*jshint evil: true */ | ||
var filter = eval('(function() { return ' + | ||
@@ -736,24 +785,10 @@ ddoc.filters[filterName[1]] + ' })()'); | ||
function fetchChanges() { | ||
txn = idb.transaction([DOC_STORE, BY_SEQ_STORE]); | ||
txn.oncomplete = onTxnComplete; | ||
var req = descending | ||
? txn.objectStore(BY_SEQ_STORE) | ||
.openCursor(IDBKeyRange.lowerBound(opts.since, true), descending) | ||
: txn.objectStore(BY_SEQ_STORE) | ||
.openCursor(IDBKeyRange.lowerBound(opts.since, true)); | ||
req.onsuccess = onsuccess; | ||
req.onerror = onerror; | ||
} | ||
function onsuccess(event) { | ||
if (!event.target.result) { | ||
if (opts.continuous && !opts.cancelled) { | ||
IdbPouch.Changes.addListener(name, id, api, opts); | ||
} | ||
// Filter out null results casued by deduping | ||
for (var i = 0, l = results.length; i < l; i++ ) { | ||
var result = results[i]; | ||
if (result) dedupResults.push(result); | ||
if (result) { | ||
dedupResults.push(result); | ||
} | ||
} | ||
@@ -766,5 +801,7 @@ return false; | ||
// Try to pre-emptively dedup to save us a bunch of idb calls | ||
var changeId = cursor.value._id, changeIdIndex = resultIndices[changeId]; | ||
var changeId = cursor.value._id; | ||
var changeIdIndex = resultIndices[changeId]; | ||
if (changeIdIndex !== undefined) { | ||
results[changeIdIndex].seq = cursor.key; // update so it has the later sequence number | ||
results[changeIdIndex].seq = cursor.key; | ||
// update so it has the later sequence number | ||
results.push(results[changeIdIndex]); | ||
@@ -784,9 +821,10 @@ results[changeIdIndex] = null; | ||
var mainRev = Pouch.merge.winningRev(metadata); | ||
var index = txn.objectStore(BY_SEQ_STORE).index('_rev'); | ||
index.get(mainRev).onsuccess = function(docevent) { | ||
var key = metadata.id + "::" + mainRev; | ||
var index = txn.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); | ||
index.get(key).onsuccess = function(docevent) { | ||
var doc = docevent.target.result; | ||
var changeList = [{rev: mainRev}] | ||
var changeList = [{rev: mainRev}]; | ||
if (opts.style === 'all_docs') { | ||
// console.log('all docs', changeList, collectLeaves(metadata.rev_tree)); | ||
changeList = collectLeaves(metadata.rev_tree); | ||
changeList = Pouch.merge.collectLeaves(metadata.rev_tree) | ||
.map(function(x) { return {rev: x.rev}; }); | ||
} | ||
@@ -797,4 +835,5 @@ var change = { | ||
changes: changeList, | ||
doc: doc, | ||
doc: doc | ||
}; | ||
if (isDeleted(metadata, mainRev)) { | ||
@@ -804,3 +843,4 @@ change.deleted = true; | ||
if (opts.conflicts) { | ||
change.doc._conflicts = collectConflicts(metadata.rev_tree, metadata.deletions); | ||
change.doc._conflicts = Pouch.merge.collectConflicts(metadata) | ||
.map(function(x) { return x.id; }); | ||
} | ||
@@ -816,46 +856,18 @@ | ||
cursor['continue'](); | ||
} | ||
}; | ||
}; | ||
}; | ||
} | ||
function onTxnComplete() { | ||
dedupResults.map(function(c) { | ||
if (opts.filter && !opts.filter.apply(this, [c.doc])) { | ||
return; | ||
} | ||
if (!opts.include_docs) { | ||
delete c.doc; | ||
} | ||
if (c.seq > opts.since) { | ||
opts.since = c.seq; | ||
call(opts.onChange, c); | ||
} | ||
}); | ||
if (!opts.continuous || (opts.continuous && !opts.cancelled)) { | ||
call(opts.complete, null, {results: dedupResults}); | ||
} | ||
}; | ||
dedupResults.map(filterChange(opts)); | ||
call(opts.complete, null, {results: dedupResults}); | ||
} | ||
function onerror(error) { | ||
if (opts.continuous && !opts.cancelled) { | ||
IdbPouch.Changes.addListener(name, id, opts); | ||
} | ||
else { | ||
call(opts.complete); | ||
} | ||
}; | ||
if (opts.continuous) { | ||
return { | ||
cancel: function() { | ||
if (Pouch.DEBUG) | ||
console.log(name + ': Cancel Changes Feed'); | ||
opts.cancelled = true; | ||
IdbPouch.Changes.removeListener(name, id); | ||
} | ||
} | ||
// TODO: shouldn't we pass some params here? | ||
call(opts.complete); | ||
} | ||
}; | ||
api.close = function(callback) { | ||
api._close = function(callback) { | ||
if (idb === null) { | ||
@@ -871,2 +883,49 @@ return call(callback, Pouch.Errors.NOT_OPEN); | ||
api._getRevisionTree = function(docId, callback) { | ||
var txn = idb.transaction([DOC_STORE], 'readonly'); | ||
var req = txn.objectStore(DOC_STORE).get(docId); | ||
req.onsuccess = function (event) { | ||
var doc = event.target.result; | ||
if (!doc) { | ||
call(callback, Pouch.Errors.MISSING_DOC); | ||
} else { | ||
call(callback, null, doc.rev_tree); | ||
} | ||
}; | ||
}; | ||
// This function removes revisions of document docId | ||
// which are listed in revs and sets this document | ||
// revision to to rev_tree | ||
api._doCompaction = function(docId, rev_tree, revs, callback) { | ||
var txn = idb.transaction([DOC_STORE, BY_SEQ_STORE], IDBTransaction.READ_WRITE); | ||
var index = txn.objectStore(DOC_STORE); | ||
index.get(docId).onsuccess = function(event) { | ||
var metadata = event.target.result; | ||
metadata.rev_tree = rev_tree; | ||
var count = revs.length; | ||
revs.forEach(function(rev) { | ||
var index = txn.objectStore(BY_SEQ_STORE).index('_doc_id_rev'); | ||
var key = docId + "::" + rev; | ||
index.getKey(key).onsuccess = function(e) { | ||
var seq = e.target.result; | ||
if (!seq) { | ||
return; | ||
} | ||
var req = txn.objectStore(BY_SEQ_STORE)['delete'](seq); | ||
count--; | ||
if (!count) { | ||
txn.objectStore(DOC_STORE).put(metadata); | ||
} | ||
}; | ||
}); | ||
}; | ||
txn.oncomplete = function() { | ||
call(callback); | ||
}; | ||
}; | ||
return api; | ||
@@ -876,11 +935,9 @@ }; | ||
IdbPouch.valid = function idb_valid() { | ||
if (!document.location.host) { | ||
console.error('indexedDB cannot be used in pages served from the filesystem'); | ||
} | ||
return !!window.indexedDB && !!document.location.host; | ||
return !!indexedDB; | ||
}; | ||
IdbPouch.destroy = function idb_destroy(name, callback) { | ||
if (Pouch.DEBUG) | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Delete Database'); | ||
} | ||
IdbPouch.Changes.clearListeners(name); | ||
@@ -896,4 +953,4 @@ var req = indexedDB.deleteDatabase(name); | ||
IdbPouch.Changes = Changes(); | ||
IdbPouch.Changes = new Changes(); | ||
Pouch.adapter('idb', IdbPouch); |
@@ -1,12 +0,8 @@ | ||
/* | ||
* A LevelDB adapter for Pouchdb | ||
* based heavily on the pouch.idb.js IndexedDB adapter | ||
* | ||
* John Chesley <john@chesl.es> | ||
* September 2012 | ||
*/ | ||
/*globals extend: true, isDeleted: true, isLocalId: true */ | ||
/*globals Buffer: true */ | ||
var pouchdir = '../' | ||
, Pouch = require(pouchdir + 'pouch.js') | ||
'use strict'; | ||
var pouchdir = '../'; | ||
var Pouch = require(pouchdir + 'pouch.js'); | ||
var call = Pouch.utils.call; | ||
@@ -18,7 +14,7 @@ | ||
var path = require('path') | ||
, fs = require('fs') | ||
, crypto = require('crypto') | ||
, EventEmitter = require('events').EventEmitter | ||
, levelup = require('levelup') | ||
var path = require('path'); | ||
var fs = require('fs'); | ||
var crypto = require('crypto'); | ||
var EventEmitter = require('events').EventEmitter; | ||
var levelup = require('levelup'); | ||
@@ -29,3 +25,3 @@ var error = function(callback, message) { | ||
}); | ||
} | ||
}; | ||
@@ -35,2 +31,3 @@ var DOC_STORE = 'document-store'; | ||
var ATTACH_STORE = 'attach-store'; | ||
var ATTACH_BINARY_STORE = 'attach-binary-store'; | ||
@@ -55,37 +52,18 @@ // leveldb barks if we try to open a db multiple times | ||
error: err, | ||
reason: err.message, | ||
reason: err.message | ||
}); | ||
} | ||
}; | ||
} | ||
LevelPouch = module.exports = function(opts, callback) { | ||
var opened = false | ||
, api = {} | ||
, update_seq = 0 | ||
, doc_count = 0 | ||
, stores = {} | ||
, name = opts.name | ||
, change_emitter = CHANGES[name] || new EventEmitter(); | ||
var LevelPouch = function(opts, callback) { | ||
var opened = false; | ||
var api = {}; | ||
var update_seq = 0; | ||
var doc_count = 0; | ||
var stores = {}; | ||
var name = opts.name; | ||
var change_emitter = CHANGES[name] || new EventEmitter(); | ||
CHANGES[name] = change_emitter; | ||
fs.stat(opts.name, function(err, stats) { | ||
if (err && err.code == 'ENOENT') { | ||
// db directory doesn't exist | ||
fs.mkdir(opts.name, initstores); | ||
} | ||
else if (stats.isDirectory()) { | ||
initstores(); | ||
} | ||
else { | ||
// error | ||
} | ||
function initstores() { | ||
initstore(DOC_STORE, 'json'); | ||
initstore(BY_SEQ_STORE, 'json'); | ||
initstore(ATTACH_STORE, 'json'); | ||
} | ||
}); | ||
function initstore(store_name, encoding) { | ||
@@ -96,13 +74,9 @@ var dbpath = path.resolve(path.join(opts.name, store_name)); | ||
// createIfMissing = true by default | ||
opts.createIfMissing = opts.createIfMissing === undefined ? true : opts.createIfMissing; | ||
opts.createIfMissing = opts.createIfMissing === undefined ? | ||
true : opts.createIfMissing; | ||
if (STORES[dbpath] !== undefined) { | ||
setup_store(null, STORES[dbpath]); | ||
} | ||
else { | ||
levelup(dbpath, opts, setup_store); | ||
} | ||
function setup_store(err, ldb) { | ||
if (stores.err) return; | ||
if (stores.err) { | ||
return; | ||
} | ||
if (err) { | ||
@@ -118,3 +92,4 @@ stores.err = err; | ||
!stores[BY_SEQ_STORE] || | ||
!stores[ATTACH_STORE]) { | ||
!stores[ATTACH_STORE] || | ||
!stores[ATTACH_BINARY_STORE]) { | ||
return; | ||
@@ -125,2 +100,9 @@ } | ||
function finish() { | ||
if (doc_count >= 0 && update_seq >= 0) { | ||
opened = true; | ||
process.nextTick(function() { call(callback, null, api); }); | ||
} | ||
} | ||
stores[BY_SEQ_STORE].get(DOC_COUNT_KEY, function(err, value) { | ||
@@ -145,12 +127,30 @@ if (!err) { | ||
}); | ||
} | ||
function finish() { | ||
if (doc_count >= 0 && update_seq >= 0) { | ||
opened = true; | ||
process.nextTick(function() { call(callback, null, api) }); | ||
} | ||
} | ||
}; | ||
if (STORES[dbpath] !== undefined) { | ||
setup_store(null, STORES[dbpath]); | ||
} | ||
else { | ||
levelup(dbpath, opts, setup_store); | ||
} | ||
} | ||
fs.stat(opts.name, function(err, stats) { | ||
function initstores() { | ||
initstore(DOC_STORE, 'json'); | ||
initstore(BY_SEQ_STORE, 'json'); | ||
initstore(ATTACH_STORE, 'json'); | ||
initstore(ATTACH_BINARY_STORE, 'binary'); | ||
} | ||
if (err && err.code === 'ENOENT') { | ||
// db directory doesn't exist | ||
fs.mkdir(opts.name, initstores); | ||
} | ||
else if (stats.isDirectory()) { | ||
initstores(); | ||
} | ||
else { | ||
// error | ||
} | ||
}); | ||
@@ -164,60 +164,24 @@ api.type = function() { | ||
return opts.name; | ||
} | ||
}; | ||
api.info = function(callback) { | ||
api._info = function(callback) { | ||
return call(callback, null, { | ||
name: opts.name, | ||
db_name: opts.name, | ||
doc_count: doc_count, | ||
update_seq: update_seq, | ||
update_seq: update_seq | ||
}); | ||
} | ||
}; | ||
api.get = function(id, opts, callback) { | ||
if (opts instanceof Function) { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
id = Pouch.utils.parseDocId(id); | ||
if (id.attachmentId !== '') { | ||
return api.getAttachment(id, {decode: true}, callback); | ||
} | ||
api._get = function(id, opts, callback) { | ||
stores[DOC_STORE].get(id.docId, function(err, metadata) { | ||
if (err || !metadata || (isDeleted(metadata) && !opts.rev)) { | ||
if (err || !metadata){ | ||
return call(callback, Pouch.Errors.MISSING_DOC); | ||
} | ||
if (opts.open_revs) { | ||
if (opts.open_revs === "all") { | ||
leaves = collectLeaves(metadata.rev_tree).map(function(leaf){ | ||
return leaf.rev; | ||
}); | ||
} else { | ||
leaves = opts.open_revs; // should be some validation here | ||
} | ||
var result = []; | ||
var count = leaves.length; | ||
leaves.forEach(function(leaf){ | ||
api.get(id.docId, {rev: leaf}, function(err, doc){ | ||
if (!err) { | ||
result.push({ok: doc}); | ||
} else { | ||
result.push({missing: leaf}); | ||
} | ||
count--; | ||
if(!count) { | ||
call(callback, null, result); | ||
} | ||
}); | ||
}); | ||
return; // open_revs can be used only with revs | ||
if (isDeleted(metadata) && !opts.rev) { | ||
return call(callback, Pouch.error(Pouch.Errors.MISSING_DOC, "deleted")); | ||
} | ||
var seq = opts.rev | ||
? metadata.rev_map[opts.rev] | ||
: metadata.seq; | ||
var rev = Pouch.merge.winningRev(metadata); | ||
rev = opts.rev ? opts.rev : rev; | ||
var seq = metadata.rev_map[rev]; | ||
@@ -232,29 +196,2 @@ stores[BY_SEQ_STORE].get(seq, function(err, doc) { | ||
if (opts.revs) { | ||
var path = Pouch.utils.arrayFirst( | ||
Pouch.utils.rootToLeaf(metadata.rev_tree), | ||
function(arr) { | ||
return arr.ids.indexOf(doc._rev.split('-')[1]) !== -1 | ||
} | ||
); | ||
path.ids.reverse(); | ||
doc._revisions = { | ||
start: (path.pos + path.ids.length) - 1, | ||
ids: path.ids | ||
}; | ||
} | ||
if (opts.revs_info) { | ||
doc._revs_info = metadata.rev_tree.reduce(function(prev, current) { | ||
return prev.concat(Pouch.utils.collectRevs(current)); | ||
}, []); | ||
} | ||
if (opts.conflicts) { | ||
var conflicts = Pouch.utils.collectConflicts(metadata.rev_tree, metadata.deletions); | ||
if (conflicts.length) { | ||
doc._conflicts = conflicts; | ||
} | ||
} | ||
if (opts.attachments && doc._attachments) { | ||
@@ -265,7 +202,7 @@ var attachments = Object.keys(doc._attachments); | ||
attachments.forEach(function(key) { | ||
api.getAttachment(doc._id + '/' + key, function(err, data) { | ||
api.getAttachment(doc._id + '/' + key, {encode: true}, function(err, data) { | ||
doc._attachments[key].data = data; | ||
if (++recv === attachments.length) { | ||
callback(null, doc); | ||
callback(doc, metadata); | ||
} | ||
@@ -281,16 +218,10 @@ }); | ||
} | ||
callback(null, doc); | ||
callback(doc, metadata); | ||
} | ||
}); | ||
}); | ||
} | ||
}; | ||
// not technically part of the spec, but if putAttachment has its own method... | ||
api.getAttachment = function(id, opts, callback) { | ||
if (opts instanceof Function) { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
id = id.docId ? id : Pouch.utils.parseDocId(id); | ||
api._getAttachment = function(id, opts, callback) { | ||
if (id.attachmentId === '') { | ||
@@ -309,13 +240,19 @@ return api.get(id, opts, callback); | ||
} | ||
var digest = doc._attachments[id.attachmentId].digest | ||
, type = doc._attachments[id.attachmentId].content_type | ||
var digest = doc._attachments[id.attachmentId].digest; | ||
var type = doc._attachments[id.attachmentId].content_type; | ||
stores[ATTACH_STORE].get(digest, function(err, attach) { | ||
stores[ATTACH_BINARY_STORE].get(digest, function(err, attach) { | ||
var data; | ||
if (err && err.name === 'NotFoundError') { | ||
// Empty attachment | ||
data = opts.encode ? '' : new Buffer(''); | ||
return call(callback, null, data); | ||
} | ||
if (err) { | ||
return call(callback, err); | ||
} | ||
var data = opts.decode | ||
? Pouch.utils.atob(attach.body.toString()) | ||
: attach.body.toString(); | ||
data = opts.encode ? btoa(attach) : attach; | ||
call(callback, null, data); | ||
@@ -325,27 +262,13 @@ }); | ||
}); | ||
} | ||
}; | ||
api.bulkDocs = function(bulk, opts, callback) { | ||
if (opts instanceof Function) { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
api._bulkDocs = function(req, opts, callback) { | ||
if (!bulk || !bulk.docs || bulk.docs.length < 1) { | ||
return call(callback, Pouch.Errors.MISSING_BULK_DOCS); | ||
} | ||
if (!Array.isArray(bulk.docs)) { | ||
return error(callback, new Error("docs should be an array of documents")); | ||
} | ||
var newEdits = opts.new_edits; | ||
var info = []; | ||
var docs = []; | ||
var results = []; | ||
var newEdits = opts.new_edits !== undefined ? opts.new_edits : true | ||
, info = [] | ||
, docs = [] | ||
, results = [] | ||
// parse the docs and give each a sequence number | ||
var userDocs = extend(true, [], bulk.docs); | ||
var userDocs = req.docs; | ||
info = userDocs.map(function(doc, i) { | ||
@@ -357,9 +280,2 @@ var newDoc = Pouch.utils.parseDoc(doc, newEdits); | ||
} | ||
if (doc._deleted) { | ||
if (!newDoc.metadata.deletions) { | ||
newDoc.metadata.deletions = {}; | ||
} | ||
newDoc.metadata.deletions[doc._rev.split('-')[1]] = true; | ||
} | ||
return newDoc; | ||
@@ -381,3 +297,3 @@ }); | ||
} | ||
if (!docs.length || info.metadata.id !== docs[docs.length-1].metadata.id) { | ||
if (!docs.length || !newEdits || info.metadata.id !== docs[docs.length-1].metadata.id) { | ||
return docs.push(info); | ||
@@ -388,4 +304,2 @@ } | ||
processDocs(); | ||
function processDocs() { | ||
@@ -397,3 +311,3 @@ if (docs.length === 0) { | ||
stores[DOC_STORE].get(currentDoc.metadata.id, function(err, oldDoc) { | ||
if (err && err.name == 'NotFoundError') { | ||
if (err && err.name === 'NotFoundError') { | ||
insertDoc(currentDoc, processDocs); | ||
@@ -420,3 +334,3 @@ } | ||
return callback(); | ||
}) | ||
}); | ||
}); | ||
@@ -426,4 +340,2 @@ } | ||
function updateDoc(oldDoc, docInfo, callback) { | ||
docInfo.metadata.deletions = extend(docInfo.metadata.deletions, oldDoc.deletions); | ||
var merged = Pouch.merge(oldDoc.rev_tree, docInfo.metadata.rev_tree[0], 1000); | ||
@@ -444,3 +356,3 @@ | ||
function writeDoc(doc, callback) { | ||
function writeDoc(doc, callback2) { | ||
var err = null; | ||
@@ -455,11 +367,35 @@ var recv = 0; | ||
var attachments = doc.data._attachments | ||
? Object.keys(doc.data._attachments) | ||
: []; | ||
var attachments = doc.data._attachments ? | ||
Object.keys(doc.data._attachments) : | ||
[]; | ||
function collectResults(attachmentErr) { | ||
if (!err) { | ||
if (attachmentErr) { | ||
err = attachmentErr; | ||
call(callback2, err); | ||
} else if (recv === attachments.length) { | ||
finish(); | ||
} | ||
} | ||
} | ||
function attachmentSaved(err) { | ||
recv++; | ||
collectResults(err); | ||
} | ||
for (var i=0; i<attachments.length; i++) { | ||
var key = attachments[i]; | ||
if (!doc.data._attachments[key].stub) { | ||
var data = doc.data._attachments[key].data | ||
// if data is an object, it's likely to actually be a Buffer that got JSON.stringified | ||
if (typeof data === 'object') data = new Buffer(data); | ||
var data = doc.data._attachments[key].data; | ||
// if data is a string, it's likely to actually be base64 encoded | ||
if (typeof data === 'string') { | ||
try { | ||
data = Pouch.utils.atob(data); | ||
} catch(e) { | ||
call(callback, Pouch.error(Pouch.Errors.BAD_ARG, "Attachments need to be base64 encoded")); | ||
return; | ||
} | ||
} | ||
var digest = 'md5-' + crypto.createHash('md5') | ||
@@ -470,6 +406,3 @@ .update(data || '') | ||
doc.data._attachments[key].digest = digest; | ||
saveAttachment(doc, digest, data, function (err) { | ||
recv++; | ||
collectResults(err); | ||
}); | ||
saveAttachment(doc, digest, data, attachmentSaved); | ||
} else { | ||
@@ -481,17 +414,2 @@ recv++; | ||
if(!attachments.length) { | ||
finish(); | ||
} | ||
function collectResults(attachmentErr) { | ||
if (!err) { | ||
if (attachmentErr) { | ||
err = attachmentErr; | ||
call(callback, err); | ||
} else if (recv == attachments.length) { | ||
finish(); | ||
} | ||
} | ||
} | ||
function finish() { | ||
@@ -509,6 +427,10 @@ update_seq++; | ||
results.push(doc); | ||
return saveUpdateSeq(callback); | ||
return saveUpdateSeq(callback2); | ||
}); | ||
}); | ||
} | ||
if(!attachments.length) { | ||
finish(); | ||
} | ||
} | ||
@@ -528,4 +450,5 @@ | ||
if (err && err.name !== 'NotFoundError') { | ||
if (Pouch.DEBUG) | ||
if (Pouch.DEBUG) { | ||
console.error(err); | ||
} | ||
return call(callback, err); | ||
@@ -535,3 +458,3 @@ } | ||
var ref = [docInfo.metadata.id, docInfo.metadata.rev].join('@'); | ||
var newAtt = {body: data}; | ||
var newAtt = {}; | ||
@@ -547,3 +470,3 @@ if (oldAtt) { | ||
} else { | ||
newAtt.refs = {} | ||
newAtt.refs = {}; | ||
newAtt.refs[ref] = true; | ||
@@ -553,6 +476,15 @@ } | ||
stores[ATTACH_STORE].put(digest, newAtt, function(err) { | ||
callback(err); | ||
if (err) { | ||
return console.error(err); | ||
} | ||
// do not try to store empty attachments | ||
if (data.length === 0) { | ||
return callback(err); | ||
} | ||
stores[ATTACH_BINARY_STORE].put(digest, data, function(err) { | ||
callback(err); | ||
if (err) { | ||
return console.error(err); | ||
} | ||
}); | ||
}); | ||
@@ -564,3 +496,3 @@ }); | ||
var aresults = []; | ||
results.sort(function(a, b) { return a._bulk_seq - b._bulk_seq }); | ||
results.sort(function(a, b) { return a._bulk_seq - b._bulk_seq; }); | ||
@@ -572,4 +504,4 @@ results.forEach(function(result) { | ||
} | ||
var metadata = result.metadata | ||
, rev = Pouch.merge.winningRev(metadata); | ||
var metadata = result.metadata; | ||
var rev = Pouch.merge.winningRev(metadata); | ||
@@ -579,3 +511,3 @@ aresults.push({ | ||
id: metadata.id, | ||
rev: rev, | ||
rev: rev | ||
}); | ||
@@ -590,5 +522,5 @@ | ||
seq: metadata.seq, | ||
changes: Pouch.utils.collectLeaves(metadata.rev_tree), | ||
changes: Pouch.merge.collectLeaves(metadata.rev_tree), | ||
doc: result.data | ||
} | ||
}; | ||
change.doc._rev = rev; | ||
@@ -606,35 +538,22 @@ | ||
} | ||
} | ||
api.allDocs = function(opts, callback) { | ||
if (opts instanceof Function) { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if ('keys' in opts) { | ||
if ('startkey' in opts) { | ||
call(callback, extend({ | ||
reason: 'Query parameter `start_key` is not compatible with multi-get' | ||
}, Pouch.Errors.QUERY_PARSE_ERROR)); | ||
return; | ||
} | ||
if ('endkey' in opts) { | ||
call(callback, extend({ | ||
reason: 'Query parameter `end_key` is not compatible with multi-get' | ||
}, Pouch.Errors.QUERY_PARSE_ERROR)); | ||
return; | ||
} | ||
} | ||
processDocs(); | ||
}; | ||
api._allDocs = function(opts, callback) { | ||
var readstreamOpts = { | ||
reverse: false, | ||
start: '-1', | ||
} | ||
start: '-1' | ||
}; | ||
if ('startkey' in opts && opts.startkey) | ||
if ('startkey' in opts && opts.startkey) { | ||
readstreamOpts.start = opts.startkey; | ||
if ('endkey' in opts && opts.endkey) | ||
} | ||
if ('endkey' in opts && opts.endkey) { | ||
readstreamOpts.end = opts.endkey; | ||
if ('descending' in opts && opts.descending) | ||
} | ||
if ('descending' in opts && opts.descending) { | ||
readstreamOpts.reverse = true; | ||
} | ||
@@ -660,3 +579,3 @@ var results = []; | ||
if (opts.conflicts) { | ||
doc.doc._conflicts = Pouch.utils.collectConflicts(metadata.rev_tree, metadata.deletions); | ||
doc.doc._conflicts = Pouch.merge.collectConflicts(metadata); | ||
} | ||
@@ -678,10 +597,11 @@ } | ||
} | ||
var metadata = entry.value; | ||
if (opts.include_docs) { | ||
var seq = entry.value.seq; | ||
var seq = metadata.rev_map[Pouch.merge.winningRev(metadata)]; | ||
stores[BY_SEQ_STORE].get(seq, function(err, data) { | ||
allDocsInner(entry.value, data); | ||
allDocsInner(metadata, data); | ||
}); | ||
} | ||
else { | ||
allDocsInner(entry.value); | ||
allDocsInner(metadata); | ||
} | ||
@@ -710,27 +630,13 @@ }); | ||
total_rows: results.length, | ||
rows: results | ||
rows: ('limit' in opts) ? results.slice(0, opts.limit) : results | ||
}); | ||
}); | ||
} | ||
}; | ||
api.changes = function(opts) { | ||
api._changes = function(opts) { | ||
var descending = 'descending' in opts ? opts.descending : false | ||
, results = [] | ||
, changeListener | ||
var descending = 'descending' in opts ? opts.descending : false; | ||
var results = []; | ||
var changeListener; | ||
// fetch a filter from a design doc | ||
if (opts.filter && typeof opts.filter === 'string') { | ||
var filtername = opts.filter.split('/'); | ||
api.get('_design/'+filtername[0], function(err, design) { | ||
var filter = eval('(function() { return ' + | ||
design.filters[filtername[1]] + '})()'); | ||
opts.filter = filter; | ||
fetchChanges(); | ||
}); | ||
} | ||
else { | ||
fetchChanges(); | ||
} | ||
function fetchChanges() { | ||
@@ -742,7 +648,9 @@ var streamOpts = { | ||
if (!streamOpts.reverse) { | ||
streamOpts.start = opts.since | ||
? opts.since + 1 | ||
: 0; | ||
streamOpts.start = opts.since ? opts.since + 1 : 0; | ||
} | ||
if (opts.limit) { | ||
streamOpts.limit = opts.limit; | ||
} | ||
var changeStream = stores[BY_SEQ_STORE].readStream(streamOpts); | ||
@@ -763,3 +671,4 @@ changeStream | ||
seq: metadata.seq, | ||
changes: Pouch.utils.collectLeaves(metadata.rev_tree), | ||
changes: Pouch.merge.collectLeaves(metadata.rev_tree) | ||
.map(function(x) { return {rev: x.rev}; }), | ||
doc: data.value | ||
@@ -770,14 +679,13 @@ }; | ||
if (isDeleted(metadata)) { | ||
if (isDeleted(metadata)) { | ||
change.deleted = true; | ||
} | ||
if (opts.conflicts) { | ||
change.doc._conflicts = Pouch.utils.collectConflicts(metadata.rev_tree, metadata.deletions); | ||
change.doc._conflicts = Pouch.merge.collectConflicts(metadata); | ||
} | ||
// dedupe changes (TODO: more efficient way to accomplish this?) | ||
results = results.filter(function(doc) { | ||
return doc.id !== change.id; | ||
}); | ||
results.push(change); | ||
// Ensure duplicated dont overwrite winning rev | ||
if (+data.key === metadata.rev_map[change.doc._rev]) { | ||
results.push(change); | ||
} | ||
}); | ||
@@ -790,3 +698,3 @@ }) | ||
.on('close', function() { | ||
changeListener = Pouch.utils.filterChange(opts) | ||
changeListener = Pouch.utils.filterChange(opts); | ||
if (opts.continuous && !opts.cancelled) { | ||
@@ -798,18 +706,34 @@ change_emitter.on('change', changeListener); | ||
call(opts.complete, null, {results: results}); | ||
}) | ||
}); | ||
} | ||
// fetch a filter from a design doc | ||
if (opts.filter && typeof opts.filter === 'string') { | ||
var filtername = opts.filter.split('/'); | ||
api.get('_design/'+filtername[0], function(err, design) { | ||
/*jshint evil: true */ | ||
var filter = eval('(function() { return ' + | ||
design.filters[filtername[1]] + '})()'); | ||
opts.filter = filter; | ||
fetchChanges(); | ||
}); | ||
} | ||
else { | ||
fetchChanges(); | ||
} | ||
if (opts.continuous) { | ||
return { | ||
cancel: function() { | ||
if (Pouch.DEBUG) | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Cancel Changes Feed'); | ||
} | ||
opts.cancelled = true; | ||
change_emitter.removeListener('change', changeListener); | ||
} | ||
} | ||
}; | ||
} | ||
} | ||
}; | ||
api.close = function(callback) { | ||
api._close = function(callback) { | ||
if (!opened) { | ||
@@ -824,6 +748,7 @@ return call(callback, Pouch.Errors.NOT_OPEN); | ||
path.join(dbpath, ATTACH_STORE), | ||
path.join(dbpath, ATTACH_BINARY_STORE) | ||
]; | ||
var closed = 0; | ||
stores.map(function(path) { | ||
var store = STORES[path] | ||
var store = STORES[path]; | ||
if (store) { | ||
@@ -850,7 +775,80 @@ store.close(function() { | ||
api._getRevisionTree = function(docId, callback){ | ||
stores[DOC_STORE].get(docId, function(err, metadata) { | ||
if (err) { | ||
call(callback, Pouch.Errors.MISSING_DOC); | ||
} else { | ||
call(callback, null, metadata.rev_tree); | ||
} | ||
}); | ||
}; | ||
api._doCompaction = function(docId, rev_tree, revs, callback) { | ||
stores[DOC_STORE].get(docId, function(err, metadata) { | ||
var seqs = metadata.rev_map; // map from rev to seq | ||
metadata.rev_tree = rev_tree; | ||
var count = revs.length; | ||
function done() { | ||
count--; | ||
if (!count) { | ||
callback(); | ||
} | ||
} | ||
if (!count) { | ||
callback(); | ||
} | ||
stores[DOC_STORE].put(metadata.id, metadata, function() { | ||
revs.forEach(function(rev) { | ||
var seq = seqs[rev]; | ||
if (!seq) { | ||
done(); | ||
return; | ||
} | ||
stores[BY_SEQ_STORE].del(seq, function(err) { | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; | ||
return api; | ||
} | ||
}; | ||
LevelPouch.valid = function() { | ||
return typeof module !== undefined && module.exports; | ||
}; | ||
// recursive fs.rmdir for Pouch.destroy. Use with care. | ||
function rmdir(dir, callback) { | ||
fs.readdir(dir, function rmfiles(err, files) { | ||
if (err) { | ||
if (err.code === 'ENOTDIR') { | ||
return fs.unlink(dir, callback); | ||
} | ||
else if (callback) { | ||
return callback(err); | ||
} | ||
else { | ||
return; | ||
} | ||
} | ||
var count = files.length; | ||
if (count === 0) { | ||
return fs.rmdir(dir, callback); | ||
} | ||
files.forEach(function(file) { | ||
var todel = path.join(dir, file); | ||
rmdir(todel, function(err) { | ||
count--; | ||
if (count <= 0) { | ||
fs.rmdir(dir, callback); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
@@ -865,6 +863,7 @@ | ||
path.join(dbpath, ATTACH_STORE), | ||
path.join(dbpath, ATTACH_BINARY_STORE) | ||
]; | ||
var closed = 0; | ||
stores.map(function(path) { | ||
var store = STORES[path] | ||
var store = STORES[path]; | ||
if (store) { | ||
@@ -895,36 +894,7 @@ store.close(function() { | ||
} | ||
}; | ||
} | ||
Pouch.adapter('ldb', LevelPouch); | ||
Pouch.adapter('leveldb', LevelPouch); | ||
// recursive fs.rmdir for Pouch.destroy. Use with care. | ||
function rmdir(dir, callback) { | ||
fs.readdir(dir, function rmfiles(err, files) { | ||
if (err) { | ||
if (err.code == 'ENOTDIR') { | ||
return fs.unlink(dir, callback); | ||
} | ||
else if (callback) { | ||
return callback(err); | ||
} | ||
else { | ||
return; | ||
} | ||
} | ||
var count = files.length; | ||
if (count == 0) { | ||
return fs.rmdir(dir, callback); | ||
} | ||
files.forEach(function(file) { | ||
var todel = path.join(dir, file); | ||
rmdir(todel, function(err) { | ||
count--; | ||
if (count <= 0) { | ||
fs.rmdir(dir, callback); | ||
} | ||
}); | ||
}) | ||
}); | ||
} | ||
module.exports = LevelPouch; |
@@ -1,3 +0,7 @@ | ||
"use strict"; | ||
/*globals call: false, extend: false, parseDoc: false, Crypto: false */ | ||
/*globals isLocalId: false, isDeleted: false, Changes: false, filterChange: false */ | ||
/*global isCordova*/ | ||
'use strict'; | ||
function quote(str) { | ||
@@ -34,2 +38,3 @@ return "'" + str + "'"; | ||
var update_seq = 0; | ||
var instanceId = null; | ||
var name = opts.name; | ||
@@ -46,28 +51,48 @@ | ||
db.transaction(function (tx) { | ||
var meta = 'CREATE TABLE IF NOT EXISTS ' + META_STORE + | ||
' (update_seq)'; | ||
var attach = 'CREATE TABLE IF NOT EXISTS ' + ATTACH_STORE + | ||
' (digest, json)'; | ||
var doc = 'CREATE TABLE IF NOT EXISTS ' + DOC_STORE + | ||
' (id unique, seq, json, winningseq)'; | ||
var seq = 'CREATE TABLE IF NOT EXISTS ' + BY_SEQ_STORE + | ||
' (seq INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, rev UNIQUE, json)'; | ||
function setup(){ | ||
db.transaction(function (tx) { | ||
var meta = 'CREATE TABLE IF NOT EXISTS ' + META_STORE + | ||
' (update_seq, dbid)'; | ||
var attach = 'CREATE TABLE IF NOT EXISTS ' + ATTACH_STORE + | ||
' (digest, json, body BLOB)'; | ||
var doc = 'CREATE TABLE IF NOT EXISTS ' + DOC_STORE + | ||
' (id unique, seq, json, winningseq)'; | ||
var seq = 'CREATE TABLE IF NOT EXISTS ' + BY_SEQ_STORE + | ||
' (seq INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, doc_id_rev UNIQUE, json)'; | ||
tx.executeSql(attach); | ||
tx.executeSql(doc); | ||
tx.executeSql(seq); | ||
tx.executeSql(meta); | ||
tx.executeSql(attach); | ||
tx.executeSql(doc); | ||
tx.executeSql(seq); | ||
tx.executeSql(meta); | ||
var sql = 'SELECT update_seq FROM ' + META_STORE; | ||
tx.executeSql(sql, [], function(tx, result) { | ||
if (!result.rows.length) { | ||
var initSeq = 'INSERT INTO ' + META_STORE + ' (update_seq) VALUES (?)'; | ||
tx.executeSql(initSeq, [0]); | ||
return; | ||
} | ||
update_seq = result.rows.item(0).update_seq; | ||
}); | ||
}, unknownError(callback), dbCreated); | ||
var updateseq = 'SELECT update_seq FROM ' + META_STORE; | ||
tx.executeSql(updateseq, [], function(tx, result) { | ||
if (!result.rows.length) { | ||
var initSeq = 'INSERT INTO ' + META_STORE + ' (update_seq) VALUES (?)'; | ||
var newId = Math.uuid(); | ||
tx.executeSql(initSeq, [0]); | ||
return; | ||
} | ||
update_seq = result.rows.item(0).update_seq; | ||
}); | ||
var dbid = 'SELECT dbid FROM ' + META_STORE; | ||
tx.executeSql(dbid, [], function(tx, result) { | ||
if (!result.rows.length) { | ||
var initDb = 'INSERT INTO ' + META_STORE + ' (dbid) VALUES (?)'; | ||
var newId = Math.uuid(); | ||
tx.executeSql(initDb, [newId]); | ||
return; | ||
} | ||
instanceId = result.rows.item(0).dbid; | ||
}); | ||
}, unknownError(callback), dbCreated); | ||
} | ||
if (isCordova()){ | ||
//to wait until custom api is made in pouch.adapters before doing setup | ||
window.addEventListener(name + "_pouch", setup, false); | ||
} else { | ||
setup(); | ||
} | ||
api.type = function() { | ||
@@ -78,11 +103,6 @@ return 'websql'; | ||
api.id = function() { | ||
var id = localJSON.get(name + '_id', null); | ||
if (id === null) { | ||
id = Math.uuid(); | ||
localJSON.set(name + '_id', id); | ||
} | ||
return id; | ||
return instanceId; | ||
}; | ||
api.info = function(callback) { | ||
api._info = function(callback) { | ||
db.transaction(function(tx) { | ||
@@ -100,18 +120,7 @@ var sql = 'SELECT COUNT(id) AS count FROM ' + DOC_STORE; | ||
api.bulkDocs = function idb_bulkDocs(req, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
api._bulkDocs = function idb_bulkDocs(req, opts, callback) { | ||
if (!req.docs) { | ||
return call(callback, Pouch.Errors.MISSING_BULK_DOCS); | ||
} | ||
var newEdits = opts.new_edits; | ||
var userDocs = req.docs; | ||
var newEdits = 'new_edits' in opts ? opts.new_edits : true; | ||
var userDocs = extend(true, [], req.docs); | ||
// Parse the docs, give them a sequence number for the result | ||
@@ -121,8 +130,2 @@ var docInfos = userDocs.map(function(doc, i) { | ||
newDoc._bulk_seq = i; | ||
if (doc._deleted) { | ||
if (!newDoc.metadata.deletions) { | ||
newDoc.metadata.deletions = {}; | ||
} | ||
newDoc.metadata.deletions[doc._rev.split('-')[1]] = true; | ||
} | ||
return newDoc; | ||
@@ -148,3 +151,3 @@ }); | ||
} | ||
if (!docs.length || docInfo.metadata.id !== docs[0].metadata.id) { | ||
if (!docs.length || !newEdits || docInfo.metadata.id !== docs[0].metadata.id) { | ||
return docs.unshift(docInfo); | ||
@@ -186,3 +189,3 @@ } | ||
webSqlPouch.Changes.notify(name); | ||
localStorage[name] = (localStorage[name] === "a") ? "b" : "a"; | ||
webSqlPouch.Changes.notifyLocalWindows(name); | ||
}); | ||
@@ -193,3 +196,80 @@ }); | ||
function preprocessAttachment(att, finish) { | ||
if (att.stub) { | ||
return finish(); | ||
} | ||
if (typeof att.data === 'string') { | ||
try { | ||
att.data = atob(att.data); | ||
} catch(e) { | ||
return call(callback, Pouch.error(Pouch.Errors.BAD_ARG, "Attachments need to be base64 encoded")); | ||
} | ||
att.digest = 'md5-' + Crypto.MD5(att.data); | ||
return finish(); | ||
} | ||
var reader = new FileReader(); | ||
reader.onloadend = function(e) { | ||
att.data = this.result; | ||
att.digest = 'md5-' + Crypto.MD5(this.result); | ||
finish(); | ||
}; | ||
reader.readAsBinaryString(att.data); | ||
} | ||
function preprocessAttachments(callback) { | ||
if (!docInfos.length) { | ||
return callback(); | ||
} | ||
var docv = 0; | ||
var recv = 0; | ||
docInfos.forEach(function(docInfo) { | ||
var attachments = docInfo.data && docInfo.data._attachments ? | ||
Object.keys(docInfo.data._attachments) : []; | ||
if (!attachments.length) { | ||
return done(); | ||
} | ||
function processedAttachment() { | ||
recv++; | ||
if (recv === attachments.length) { | ||
done(); | ||
} | ||
} | ||
for (var key in docInfo.data._attachments) { | ||
preprocessAttachment(docInfo.data._attachments[key], processedAttachment); | ||
} | ||
}); | ||
function done() { | ||
docv++; | ||
if (docInfos.length === docv) { | ||
callback(); | ||
} | ||
} | ||
} | ||
function writeDoc(docInfo, callback, isUpdate) { | ||
function finish() { | ||
var data = docInfo.data; | ||
var sql = 'INSERT INTO ' + BY_SEQ_STORE + ' (doc_id_rev, json) VALUES (?, ?);'; | ||
tx.executeSql(sql, [data._id + "::" + data._rev, | ||
JSON.stringify(data)], dataWritten); | ||
} | ||
function collectResults(attachmentErr) { | ||
if (!err) { | ||
if (attachmentErr) { | ||
err = attachmentErr; | ||
call(callback, err); | ||
} else if (recv === attachments.length) { | ||
finish(); | ||
} | ||
} | ||
} | ||
var err = null; | ||
@@ -208,12 +288,13 @@ var recv = 0; | ||
function attachmentSaved(err) { | ||
recv++; | ||
collectResults(err); | ||
} | ||
for (var key in docInfo.data._attachments) { | ||
if (!docInfo.data._attachments[key].stub) { | ||
var data = docInfo.data._attachments[key].data; | ||
var digest = 'md5-' + Crypto.MD5(data); | ||
delete docInfo.data._attachments[key].data; | ||
docInfo.data._attachments[key].digest = digest; | ||
saveAttachment(docInfo, digest, data, function(err) { | ||
recv++; | ||
collectResults(err); | ||
}); | ||
var digest = docInfo.data._attachments[key].digest; | ||
saveAttachment(docInfo, digest, data, attachmentSaved); | ||
} else { | ||
@@ -229,13 +310,2 @@ recv++; | ||
function collectResults(attachmentErr) { | ||
if (!err) { | ||
if (attachmentErr) { | ||
err = attachmentErr; | ||
call(callback, err); | ||
} else if (recv == attachments.length) { | ||
finish(); | ||
} | ||
} | ||
} | ||
function dataWritten(tx, result) { | ||
@@ -248,7 +318,10 @@ var seq = docInfo.metadata.seq = result.insertId; | ||
var sql = isUpdate ? | ||
'UPDATE ' + DOC_STORE + ' SET seq=?, json=?, winningseq=(SELECT seq FROM ' + BY_SEQ_STORE + ' WHERE rev=?) WHERE id=?' : | ||
'UPDATE ' + DOC_STORE + ' SET seq=?, json=?, winningseq=(SELECT seq FROM ' + | ||
BY_SEQ_STORE + ' WHERE doc_id_rev=?) WHERE id=?' : | ||
'INSERT INTO ' + DOC_STORE + ' (id, seq, winningseq, json) VALUES (?, ?, ?, ?);'; | ||
var metadataStr = JSON.stringify(docInfo.metadata); | ||
var key = docInfo.metadata.id + "::" + mainRev; | ||
var params = isUpdate ? | ||
[seq, JSON.stringify(docInfo.metadata), mainRev, docInfo.metadata.id] : | ||
[docInfo.metadata.id, seq, seq, JSON.stringify(docInfo.metadata)]; | ||
[seq, metadataStr, key, docInfo.metadata.id] : | ||
[docInfo.metadata.id, seq, seq, metadataStr]; | ||
tx.executeSql(sql, params, function(tx, result) { | ||
@@ -259,13 +332,5 @@ results.push(docInfo); | ||
} | ||
function finish() { | ||
var data = docInfo.data; | ||
var sql = 'INSERT INTO ' + BY_SEQ_STORE + ' (rev, json) VALUES (?, ?);'; | ||
tx.executeSql(sql, [data._rev, JSON.stringify(data)], dataWritten); | ||
} | ||
} | ||
function updateDoc(oldDoc, docInfo) { | ||
docInfo.metadata.deletions = extend(docInfo.metadata.deletions, oldDoc.deletions); | ||
var merged = Pouch.merge(oldDoc.rev_tree, docInfo.metadata.rev_tree[0], 1000); | ||
@@ -302,2 +367,5 @@ var inConflict = (isDeleted(oldDoc) && isDeleted(docInfo.metadata)) || | ||
} else { | ||
// if we have newEdits=false then we can update the same | ||
// document twice in a single bulk docs call | ||
fetchedDocs[id] = currentDoc.metadata; | ||
insertDoc(currentDoc); | ||
@@ -315,4 +383,4 @@ } | ||
var ref = [docInfo.metadata.id, docInfo.metadata.rev].join('@'); | ||
var newAtt = {digest: digest, body: data}; | ||
var sql = 'SELECT * FROM ' + ATTACH_STORE + ' WHERE digest=?'; | ||
var newAtt = {digest: digest}; | ||
var sql = 'SELECT digest, json FROM ' + ATTACH_STORE + ' WHERE digest=?'; | ||
tx.executeSql(sql, [digest], function(tx, result) { | ||
@@ -322,4 +390,4 @@ if (!result.rows.length) { | ||
newAtt.refs[ref] = true; | ||
sql = 'INSERT INTO ' + ATTACH_STORE + '(digest, json) VALUES (?, ?)'; | ||
tx.executeSql(sql, [digest, JSON.stringify(newAtt)], function() { | ||
sql = 'INSERT INTO ' + ATTACH_STORE + '(digest, json, body) VALUES (?, ?, ?)'; | ||
tx.executeSql(sql, [digest, JSON.stringify(newAtt), data], function() { | ||
call(callback, null); | ||
@@ -329,4 +397,4 @@ }); | ||
newAtt.refs = JSON.parse(result.rows.item(0).json).refs; | ||
sql = 'UPDATE ' + ATTACH_STORE + ' SET json=? WHERE digest=?'; | ||
tx.executeSql(sql, [JSON.stringify(newAtt), digest], function() { | ||
sql = 'UPDATE ' + ATTACH_STORE + ' SET json=?, body=? WHERE digest=?'; | ||
tx.executeSql(sql, [JSON.stringify(newAtt), data, digest], function() { | ||
call(callback, null); | ||
@@ -346,25 +414,17 @@ }); | ||
db.transaction(function(txn) { | ||
tx = txn; | ||
var ids = '(' + docs.map(function(d) { | ||
return quote(d.metadata.id); | ||
}).join(',') + ')'; | ||
var sql = 'SELECT * FROM ' + DOC_STORE + ' WHERE id IN ' + ids; | ||
tx.executeSql(sql, [], metadataFetched); | ||
}, unknownError(callback)); | ||
preprocessAttachments(function() { | ||
db.transaction(function(txn) { | ||
tx = txn; | ||
var ids = '(' + docs.map(function(d) { | ||
return quote(d.metadata.id); | ||
}).join(',') + ')'; | ||
var sql = 'SELECT * FROM ' + DOC_STORE + ' WHERE id IN ' + ids; | ||
tx.executeSql(sql, [], metadataFetched); | ||
}, unknownError(callback)); | ||
}); | ||
}; | ||
api.get = function(id, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
id = parseDocId(id); | ||
if (id.attachmentId !== '') { | ||
return api.getAttachment(id, {decode: true}, callback); | ||
} | ||
api._get = function(id, opts, callback) { | ||
var result; | ||
var leaves; | ||
var metadata; | ||
db.transaction(function(tx) { | ||
@@ -377,22 +437,12 @@ var sql = 'SELECT * FROM ' + DOC_STORE + ' WHERE id=?'; | ||
} | ||
var metadata = JSON.parse(results.rows.item(0).json); | ||
if (isDeleted(metadata, opts.rev) && !opts.rev) { | ||
result = Pouch.Errors.MISSING_DOC; | ||
metadata = JSON.parse(results.rows.item(0).json); | ||
if (isDeleted(metadata) && !opts.rev) { | ||
result = Pouch.error(Pouch.Errors.MISSING_DOC, "deleted"); | ||
return; | ||
} | ||
if (opts.open_revs) { | ||
if (opts.open_revs === "all") { | ||
leaves = collectLeaves(metadata.rev_tree).map(function(leaf){ | ||
return leaf.rev; | ||
}); | ||
} else { | ||
leaves = opts.open_revs; // should be some validation here | ||
} | ||
return; // open_revs can be used only with revs | ||
} | ||
var rev = Pouch.merge.winningRev(metadata); | ||
var key = opts.rev ? opts.rev : rev; | ||
var sql = 'SELECT * FROM ' + BY_SEQ_STORE + ' WHERE rev=?'; | ||
key = metadata.id + '::' + key; | ||
var sql = 'SELECT * FROM ' + BY_SEQ_STORE + ' WHERE doc_id_rev=?'; | ||
tx.executeSql(sql, [key], function(tx, results) { | ||
@@ -405,26 +455,2 @@ if (!results.rows.length) { | ||
if (opts.revs) { | ||
var path = arrayFirst(rootToLeaf(metadata.rev_tree), function(arr) { | ||
return arr.ids.indexOf(doc._rev.split('-')[1]) !== -1; | ||
}); | ||
path.ids.reverse(); | ||
doc._revisions = { | ||
start: (path.pos + path.ids.length) - 1, | ||
ids: path.ids | ||
}; | ||
} | ||
if (opts.revs_info) { | ||
doc._revs_info = metadata.rev_tree.reduce(function(prev, current) { | ||
return prev.concat(collectRevs(current)); | ||
}, []); | ||
} | ||
if (opts.conflicts) { | ||
var conflicts = collectConflicts(metadata.rev_tree, metadata.deletions); | ||
if (conflicts.length) { | ||
doc._conflicts = conflicts; | ||
} | ||
} | ||
if (opts.attachments && doc._attachments) { | ||
@@ -434,3 +460,3 @@ var attachments = Object.keys(doc._attachments); | ||
attachments.forEach(function(key) { | ||
api.getAttachment(doc._id + '/' + key, {txn: tx}, function(err, data) { | ||
api.getAttachment(doc._id + '/' + key, {encode: true, txn: tx}, function(err, data) { | ||
doc._attachments[key].data = data; | ||
@@ -452,53 +478,15 @@ if (++recv === attachments.length) { | ||
}); | ||
}, unknownError(callback), function() { | ||
if (leaves) { | ||
result = []; | ||
var count = leaves.length; | ||
leaves.forEach(function(leaf){ | ||
api.get(id.docId, {rev: leaf}, function(err, doc){ | ||
if (!err) { | ||
result.push({ok: doc}); | ||
} else { | ||
result.push({missing: leaf}); | ||
} | ||
count--; | ||
if(!count) { | ||
finish(); | ||
} | ||
}); | ||
}); | ||
} else { | ||
finish(); | ||
} | ||
}, unknownError(callback), function () { | ||
call(callback, result, metadata); | ||
}); | ||
function finish(){ | ||
if ('error' in result) { | ||
call(callback, result); | ||
} else { | ||
call(callback, null, result); | ||
} | ||
} | ||
}; | ||
api.allDocs = function(opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if ('keys' in opts) { | ||
if ('startkey' in opts) { | ||
call(callback, extend({ | ||
reason: 'Query parameter `start_key` is not compatible with multi-get' | ||
}, Pouch.Errors.QUERY_PARSE_ERROR)); | ||
return; | ||
} | ||
if ('endkey' in opts) { | ||
call(callback, extend({ | ||
reason: 'Query parameter `end_key` is not compatible with multi-get' | ||
}, Pouch.Errors.QUERY_PARSE_ERROR)); | ||
return; | ||
} | ||
} | ||
function makeRevs(arr) { | ||
return arr.map(function(x) { return {rev: x.rev}; }); | ||
} | ||
function makeIds(arr) { | ||
return arr.map(function(x) { return x.id; }); | ||
} | ||
api._allDocs = function(opts, callback) { | ||
var results = []; | ||
@@ -535,3 +523,3 @@ var resultsMap = {}; | ||
if (!(isLocalId(metadata.id))) { | ||
var doc = { | ||
doc = { | ||
id: metadata.id, | ||
@@ -545,3 +533,3 @@ key: metadata.id, | ||
if (opts.conflicts) { | ||
doc.doc._conflicts = collectConflicts(metadata.rev_tree, metadata.deletions); | ||
doc.doc._conflicts = makeIds(Pouch.merge.collectConflicts(metadata)); | ||
} | ||
@@ -580,12 +568,33 @@ } | ||
total_rows: results.length, | ||
rows: results | ||
rows: ('limit' in opts) ? results.slice(0, opts.limit) : results | ||
}); | ||
}); | ||
} | ||
}; | ||
api.changes = function idb_changes(opts) { | ||
api._changes = function idb_changes(opts) { | ||
if (Pouch.DEBUG) | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Start Changes Feed: continuous=' + opts.continuous); | ||
} | ||
if (!opts.since) { | ||
opts.since = 0; | ||
} | ||
if (opts.continuous) { | ||
var id = name + ':' + Math.uuid(); | ||
opts.cancelled = false; | ||
webSqlPouch.Changes.addListener(name, id, api, opts); | ||
webSqlPouch.Changes.notify(name); | ||
return { | ||
cancel: function() { | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Cancel Changes Feed'); | ||
} | ||
opts.cancelled = true; | ||
webSqlPouch.Changes.removeListener(name, id); | ||
} | ||
}; | ||
} | ||
var descending = 'descending' in opts ? opts.descending : false; | ||
@@ -598,17 +607,4 @@ descending = descending ? 'prev' : null; | ||
var results = [], resultIndices = {}, dedupResults = []; | ||
var id = name + ':' + Math.uuid(); | ||
var txn; | ||
if (opts.filter && typeof opts.filter === 'string') { | ||
var filterName = opts.filter.split('/'); | ||
api.get('_design/' + filterName[0], function(err, ddoc) { | ||
var filter = eval('(function() { return ' + | ||
ddoc.filters[filterName[1]] + ' })()'); | ||
opts.filter = filter; | ||
fetchChanges(); | ||
}); | ||
} else { | ||
fetchChanges(); | ||
} | ||
function fetchChanges() { | ||
@@ -621,2 +617,6 @@ var sql = 'SELECT ' + DOC_STORE + '.id, ' + BY_SEQ_STORE + '.seq, ' + | ||
if (opts.limit) { | ||
sql += ' LIMIT ' + opts.limit; | ||
} | ||
db.transaction(function(tx) { | ||
@@ -631,4 +631,4 @@ tx.executeSql(sql, [], function(tx, result) { | ||
seq: doc.seq, | ||
changes: collectLeaves(metadata.rev_tree), | ||
doc: JSON.parse(doc.data), | ||
changes: makeRevs(Pouch.merge.collectLeaves(metadata.rev_tree)), | ||
doc: JSON.parse(doc.data) | ||
}; | ||
@@ -640,3 +640,3 @@ change.doc._rev = Pouch.merge.winningRev(metadata); | ||
if (opts.conflicts) { | ||
change.doc._conflicts = collectConflicts(metadata.rev_tree, metadata.deletions); | ||
change.doc._conflicts = makeIds(Pouch.merge.collectConflicts(metadata)); | ||
} | ||
@@ -646,25 +646,11 @@ results.push(change); | ||
} | ||
for (var i = 0, l = results.length; i < l; i++ ) { | ||
var result = results[i]; | ||
if (result) dedupResults.push(result); | ||
for (i = 0, l = results.length; i < l; i++ ) { | ||
result = results[i]; | ||
if (result) { | ||
dedupResults.push(result); | ||
} | ||
} | ||
dedupResults.map(function(c) { | ||
if (opts.filter && !opts.filter.apply(this, [c.doc])) { | ||
return; | ||
} | ||
if (!opts.include_docs) { | ||
delete c.doc; | ||
} | ||
if (c.seq > opts.since) { | ||
opts.since = c.seq; | ||
call(opts.onChange, c); | ||
} | ||
}); | ||
dedupResults.map(filterChange(opts)); | ||
if (opts.continuous && !opts.cancelled) { | ||
webSqlPouch.Changes.addListener(name, id, api, opts); | ||
} | ||
else { | ||
call(opts.complete, null, {results: dedupResults}); | ||
} | ||
call(opts.complete, null, {results: dedupResults}); | ||
}); | ||
@@ -674,41 +660,19 @@ }); | ||
if (opts.continuous) { | ||
return { | ||
cancel: function() { | ||
if (Pouch.DEBUG) | ||
console.log(name + ': Cancel Changes Feed'); | ||
opts.cancelled = true; | ||
webSqlPouch.Changes.removeListener(name, id); | ||
} | ||
} | ||
if (opts.filter && typeof opts.filter === 'string') { | ||
var filterName = opts.filter.split('/'); | ||
api.get('_design/' + filterName[0], function(err, ddoc) { | ||
/*jshint evil: true */ | ||
var filter = eval('(function() { return ' + | ||
ddoc.filters[filterName[1]] + ' })()'); | ||
opts.filter = filter; | ||
fetchChanges(); | ||
}); | ||
} else { | ||
fetchChanges(); | ||
} | ||
}; | ||
api.getAttachment = function(id, opts, callback) { | ||
if (opts instanceof Function) { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (typeof id === 'string') { | ||
id = parseDocId(id); | ||
} | ||
api._getAttachment = function(id, opts, callback) { | ||
var res; | ||
// This can be called while we are in a current transaction, pass the context | ||
// along and dont wait for the transaction to complete here. | ||
if ('txn' in opts) { | ||
fetchAttachment(opts.txn); | ||
} else { | ||
db.transaction(fetchAttachment, unknownError(callback), function() { | ||
call(callback, null, res); | ||
}); | ||
} | ||
function postProcessDoc(data) { | ||
if (opts.decode) { | ||
return atob(data); | ||
} | ||
return data; | ||
} | ||
function fetchAttachment(tx) { | ||
@@ -723,6 +687,10 @@ var sql = 'SELECT ' + BY_SEQ_STORE + '.json AS data FROM ' + DOC_STORE + | ||
var type = attachment.content_type; | ||
var sql = 'SELECT * FROM ' + ATTACH_STORE + ' WHERE digest=?'; | ||
var sql = 'SELECT body FROM ' + ATTACH_STORE + ' WHERE digest=?'; | ||
tx.executeSql(sql, [digest], function(tx, result) { | ||
var data = JSON.parse(result.rows.item(0).json).body; | ||
res = postProcessDoc(data); | ||
var data = result.rows.item(0).body; | ||
if (opts.encode) { | ||
res = btoa(data); | ||
} else { | ||
res = new Blob([data], {type: type}); | ||
} | ||
if ('txn' in opts) { | ||
@@ -734,5 +702,54 @@ call(callback, null, res); | ||
} | ||
} | ||
// This can be called while we are in a current transaction, pass the context | ||
// along and dont wait for the transaction to complete here. | ||
if ('txn' in opts) { | ||
fetchAttachment(opts.txn); | ||
} else { | ||
db.transaction(fetchAttachment, unknownError(callback), function() { | ||
call(callback, null, res); | ||
}); | ||
} | ||
}; | ||
api._getRevisionTree = function(docId, callback) { | ||
db.transaction(function (tx) { | ||
var sql = 'SELECT json AS metadata FROM ' + DOC_STORE + ' WHERE id = ?'; | ||
tx.executeSql(sql, [docId], function(tx, result) { | ||
if (!result.rows.length) { | ||
call(callback, Pouch.Errors.MISSING_DOC); | ||
} else { | ||
var data = JSON.parse(result.rows.item(0).metadata); | ||
call(callback, null, data.rev_tree); | ||
} | ||
}); | ||
}); | ||
}; | ||
api._doCompaction = function(docId, rev_tree, revs, callback) { | ||
db.transaction(function (tx) { | ||
var sql = 'SELECT json AS metadata FROM ' + DOC_STORE + ' WHERE id = ?'; | ||
tx.executeSql(sql, [docId], function(tx, result) { | ||
if (!result.rows.length) { | ||
return call(callback); | ||
} | ||
var metadata = JSON.parse(result.rows.item(0).metadata); | ||
metadata.rev_tree = rev_tree; | ||
var sql = 'DELETE FROM ' + BY_SEQ_STORE + ' WHERE doc_id_rev IN (' + | ||
revs.map(function(rev){return quote(docId + '::' + rev);}).join(',') + ')'; | ||
tx.executeSql(sql, [], function(tx, result) { | ||
var sql = 'UPDATE ' + DOC_STORE + ' SET json = ? WHERE id = ?'; | ||
tx.executeSql(sql, [JSON.stringify(metadata), docId], function(tx, result) { | ||
callback(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; | ||
return api; | ||
} | ||
}; | ||
@@ -745,3 +762,2 @@ webSqlPouch.valid = function() { | ||
var db = openDatabase(name, POUCH_VERSION, name, POUCH_SIZE); | ||
localJSON.set(name + '_id', null); | ||
db.transaction(function (tx) { | ||
@@ -757,4 +773,4 @@ tx.executeSql('DROP TABLE IF EXISTS ' + DOC_STORE, []); | ||
webSqlPouch.Changes = Changes(); | ||
webSqlPouch.Changes = new Changes(); | ||
Pouch.adapter('websql', webSqlPouch); |
@@ -24,2 +24,6 @@ /*global Pouch: true */ | ||
if (!fun.reduce) { | ||
options.reduce = false; | ||
} | ||
function sum(values) { | ||
@@ -39,3 +43,3 @@ return values.reduce(function(a, b) { return a + b; }, 0); | ||
value: val | ||
}; | ||
}; | ||
@@ -45,2 +49,3 @@ if (options.startkey && Pouch.collate(key, options.startkey) < 0) return; | ||
if (options.key && Pouch.collate(key, options.key) !== 0) return; | ||
num_started++; | ||
@@ -73,6 +78,2 @@ if (options.include_docs) { | ||
// exclude _conflicts key by default | ||
// or to use options.conflicts if it's set when called by db.query | ||
var conflicts = ('conflicts' in options ? options.conflicts : false); | ||
//only proceed once all documents are mapped and joined | ||
@@ -88,3 +89,8 @@ var checkComplete= function(){ | ||
if (options.reduce === false) { | ||
return options.complete(null, {rows: results}); | ||
return options.complete(null, { | ||
rows: ('limit' in options) | ||
? results.slice(0, options.limit) | ||
: results, | ||
total_rows: results.length | ||
}); | ||
} | ||
@@ -106,3 +112,8 @@ | ||
}); | ||
options.complete(null, {rows: groups}); | ||
options.complete(null, { | ||
rows: ('limit' in options) | ||
? groups.slice(0, options.limit) | ||
: groups, | ||
total_rows: groups.length | ||
}); | ||
} | ||
@@ -112,3 +123,3 @@ } | ||
db.changes({ | ||
conflicts: conflicts, | ||
conflicts: true, | ||
include_docs: true, | ||
@@ -209,3 +220,6 @@ onChange: function(doc) { | ||
if (db.type() === 'http') { | ||
return httpQuery(fun, opts, callback); | ||
if (typeof fun === 'function'){ | ||
return httpQuery({map: fun}, opts, callback); | ||
} | ||
return httpQuery(fun, opts, callback); | ||
} | ||
@@ -217,2 +231,6 @@ | ||
if (typeof fun === 'function') { | ||
return viewQuery({map: fun}, opts); | ||
} | ||
var parts = fun.split('/'); | ||
@@ -219,0 +237,0 @@ db.get('_design/' + parts[0], function(err, doc) { |
@@ -12,2 +12,6 @@ /*global Pouch: true */ | ||
var isArray = Array.isArray || function(obj) { | ||
return type(obj) === "array"; | ||
}; | ||
function viewQuery(fun, options) { | ||
@@ -23,42 +27,2 @@ if (!options.complete) { | ||
// NOTE vmx 2013-01-27: I wouldn't guarantee that this function is | ||
// flawless | ||
var calculateBbox = function (geom) { | ||
var coords = geom.coordinates; | ||
if (geom.type === 'Point') { | ||
return [[coords[0], coords[0]], [coords[1], coords[1]]]; | ||
} | ||
if (geom.type === 'GeometryCollection') { | ||
coords = geom.geometries.map(function(g) { | ||
return calculateBbox(g); | ||
}); | ||
return coords.reduce(function (a, b) { | ||
var minX = Math.min(a[0], b[0]); | ||
var minY = Math.min(a[1], b[1]); | ||
var maxX = Math.max(a[2], b[0]); | ||
var maxY = Math.max(a[3], b[1]); | ||
return [[minX, maxX], [minY, maxY]]; | ||
}); | ||
} | ||
// Flatten coords as much as possible | ||
while (Array.isArray(coords[0][0])) { | ||
coords = coords.reduce(function(a, b) { | ||
return a.concat(b); | ||
}); | ||
}; | ||
return coords.reduce(function (acc, coord) { | ||
// The first element isn't a bbox yet | ||
if (acc.length === 1) { | ||
acc = [[acc[0], acc[0]], [acc[1], acc[1]]]; | ||
} | ||
var minX = Math.min(acc[0][0], coord[0]); | ||
var minY = Math.min(acc[0][1], coord[1]); | ||
var maxX = Math.max(acc[1][0], coord[0]); | ||
var maxY = Math.max(acc[1][1], coord[1]); | ||
return [[minX, maxX], [minY, maxY]]; | ||
}); | ||
}; | ||
// Make the key a proper one. If a value is a single point, transform it | ||
@@ -73,5 +37,5 @@ // to a range. If the first element (or the whole key) is a geometry, | ||
// Whole key is one geometry | ||
if (isPlainObject(key)) { | ||
if (!isArray(key) && typeof key === "object") { | ||
return { | ||
key: calculateBbox(key), | ||
key: Spatial.calculateBbox(key), | ||
geometry: key | ||
@@ -81,4 +45,4 @@ }; | ||
if (isPlainObject(key[0])) { | ||
newKey = calculateBbox(key[0]); | ||
if (!isArray(key[0]) && typeof key[0] === "object") { | ||
newKey = Spatial.calculateBbox(key[0]); | ||
geometry = key[0]; | ||
@@ -238,5 +202,4 @@ key = key.slice(1); | ||
if (typeof fun !== 'string') { | ||
var error = extend({reason: 'Querying with a function is not ' + | ||
'supported for Spatial Views'}, Pouch.Errors.INVALID_REQUEST); | ||
return call(callback, error); | ||
var error = Pouch.error( Pouch.Errors.INVALID_REQUEST, 'Querying with a function is not supported for Spatial Views'); | ||
return callback ? callback(error) : undefined; | ||
} | ||
@@ -261,2 +224,43 @@ | ||
// Store it in the Spatial object, so we can test it | ||
Spatial.calculateBbox = function (geom) { | ||
var coords = geom.coordinates; | ||
if (geom.type === 'Point') { | ||
return [[coords[0], coords[0]], [coords[1], coords[1]]]; | ||
} | ||
if (geom.type === 'GeometryCollection') { | ||
coords = geom.geometries.map(function(g) { | ||
return Spatial.calculateBbox(g); | ||
}); | ||
// Merge all bounding boxes into one big one that encloses all | ||
return coords.reduce(function (acc, bbox) { | ||
var minX = Math.min(acc[0][0], bbox[0][0]); | ||
var minY = Math.min(acc[1][0], bbox[1][0]); | ||
var maxX = Math.max(acc[0][1], bbox[0][1]); | ||
var maxY = Math.max(acc[1][1], bbox[1][1]); | ||
return [[minX, maxX], [minY, maxY]]; | ||
}); | ||
} | ||
// Flatten coords as much as possible | ||
while (Array.isArray(coords[0][0])) { | ||
coords = coords.reduce(function(a, b) { | ||
return a.concat(b); | ||
}); | ||
}; | ||
// Calculate the enclosing bounding box of all coordinates | ||
return coords.reduce(function (acc, coord) { | ||
if (acc === null) { | ||
return [[coord[0], coord[0]], [coord[1], coord[1]]]; | ||
} | ||
var minX = Math.min(acc[0][0], coord[0]); | ||
var minY = Math.min(acc[1][0], coord[1]); | ||
var maxX = Math.max(acc[0][1], coord[0]); | ||
var maxY = Math.max(acc[1][1], coord[1]); | ||
return [[minX, maxX], [minY, maxY]]; | ||
}, null); | ||
}; | ||
// Deletion is a noop since we dont store the results of the view | ||
@@ -263,0 +267,0 @@ Spatial._delete = function() { }; |
"use strict"; | ||
var visualizeRevTree = function(db) { | ||
// see: pouch.utils.js | ||
var traverseRevTree = function(revs, callback) { | ||
var toVisit = []; | ||
var head = document.getElementsByTagName("head")[0]; | ||
if (head) { | ||
var style = [ | ||
".visualizeRevTree{position: relative}", | ||
".visualizeRevTree * {margin: 0; padding: 0; font-size: 10px}", | ||
".visualizeRevTree line{stroke: #000; stroke-width: .10}", | ||
".visualizeRevTree div{position: relative; }", | ||
".visualizeRevTree circle{stroke: #000; stroke-width: .10}", | ||
".visualizeRevTree circle.leaf{fill: green}", | ||
".visualizeRevTree circle.winner{fill: red}", | ||
".visualizeRevTree circle.deleted{fill: grey}", | ||
".visualizeRevTree circle{transition: .3s}", | ||
".visualizeRevTree circle.selected{stroke-width: .3}", | ||
".visualizeRevTree div.box{background: #ddd; border: 1px solid #bbb; border-radius: 7px; padding: 7px; position: absolute;}", | ||
".visualizeRevTree .editor {width: 220px}", | ||
".visualizeRevTree .editor dt{width: 100px; height: 15px; float: left;}", | ||
".visualizeRevTree .editor dd{width: 100px; height: 15px; float: left;}", | ||
".visualizeRevTree .editor input{width: 100%; height: 100%}" | ||
]; | ||
var styleNode = document.createElement("style"); | ||
styleNode.appendChild(document.createTextNode(style.join("\n"))); | ||
head.appendChild(styleNode); | ||
} | ||
revs.forEach(function(tree) { | ||
toVisit.push({pos: tree.pos, ids: tree.ids}); | ||
}); | ||
var grid = 10; | ||
var scale = 7; | ||
var r = 1; | ||
while (toVisit.length > 0) { | ||
var node = toVisit.pop(), | ||
pos = node.pos, | ||
tree = node.ids; | ||
var newCtx = callback(tree[1].length === 0, pos, tree[0], node.ctx); | ||
tree[1].forEach(function(branch) { | ||
toVisit.push({pos: pos+1, ids: branch, ctx: newCtx}); | ||
}); | ||
// see: pouch.utils.js | ||
var revisionsToPath = function(revisions){ | ||
var tree = [revisions.ids[0], {}, []]; | ||
var i, rev; | ||
for(i = 1; i < revisions.ids.length; i++){ | ||
rev = revisions.ids[i]; | ||
tree = [rev, {}, [tree]]; | ||
} | ||
return { | ||
pos: revisions.start - revisions.ids.length + 1, | ||
ids: tree | ||
}; | ||
}; | ||
// returns minimal number i such that prefixes of lenght i are unique | ||
// ex: ["xyaaa", "xybbb", "xybccc"] -> 4 | ||
var minUniqueLength = function(arr, len){ | ||
function strCommon(a, b){ | ||
if (a === b) return a.length; | ||
var i = 0; | ||
while(++i){ | ||
if(a[i - 1] !== b[i - 1]) return i; | ||
} | ||
} | ||
var array = arr.slice(0); | ||
var com = 1; | ||
array.sort(); | ||
for (var i = 1; i < array.length; i++){ | ||
com = Math.max(com, strCommon(array[i], array[i - 1])); | ||
} | ||
return com; | ||
}; | ||
var putAfter = function(doc, prevRev, callback){ | ||
var newDoc = JSON.parse(JSON.stringify(doc)); | ||
newDoc._revisions = { | ||
start: +newDoc._rev.split('-')[0], | ||
ids: [ | ||
newDoc._rev.split('-')[1], | ||
prevRev.split('-')[1] | ||
] | ||
}; | ||
db.put(newDoc, {new_edits: false}, callback); | ||
}; | ||
var visualize = function(docId, opts, callback) { | ||
@@ -32,9 +85,9 @@ if (typeof opts === 'function') { | ||
if (isLeaf) { | ||
el.setAttributeNS(null, "fill", "green"); | ||
el.classList.add("leaf"); | ||
} | ||
if (isWinner) { | ||
el.setAttributeNS(null, "fill", "red"); | ||
el.classList.add("winner"); | ||
} | ||
if (isDeleted) { | ||
el.setAttributeNS(null, "stroke", "grey"); | ||
el.classList.add("deleted"); | ||
} | ||
@@ -50,4 +103,2 @@ circlesBox.appendChild(el); | ||
el.setAttributeNS(null, "y2", y2); | ||
el.setAttributeNS(null, "stroke", "#000"); | ||
el.setAttributeNS(null, "stroke-width", ".25"); | ||
linesBox.appendChild(el); | ||
@@ -57,3 +108,6 @@ return el; | ||
var svgNS = "http://www.w3.org/2000/svg"; | ||
var box = document.createElement('div'); | ||
box.className = "visualizeRevTree"; | ||
var svg = document.createElementNS(svgNS, "svg"); | ||
box.appendChild(svg); | ||
var linesBox = document.createElementNS(svgNS, "g"); | ||
@@ -63,16 +117,5 @@ svg.appendChild(linesBox); | ||
svg.appendChild(circlesBox); | ||
var textsBox = document.createElementNS(svgNS, "g"); | ||
svg.appendChild(textsBox); | ||
function revisionsToPath(revisions){ | ||
var tree = [revisions.ids[0], []]; | ||
var i, rev; | ||
for(i = 1; i < revisions.ids.length; i++){ | ||
rev = revisions.ids[i]; | ||
tree = [rev, [tree]]; | ||
} | ||
return { | ||
pos: revisions.start - revisions.ids.length + 1, | ||
ids: tree | ||
}; | ||
} | ||
// first we need to download all data using public API | ||
@@ -82,2 +125,3 @@ var tree = []; | ||
var winner; | ||
var allRevs = []; | ||
@@ -102,2 +146,7 @@ // consider using revs=true&open_revs=all to get everything in one query | ||
db.get(docId, {rev: res.ok._rev, revs: true}, function(err, res){ // get the whole branch of current leaf | ||
res._revisions.ids.forEach(function(rev){ | ||
if (allRevs.indexOf(rev) === -1) { | ||
allRevs.push(rev); | ||
} | ||
}); | ||
var path = revisionsToPath(res._revisions); | ||
@@ -114,9 +163,151 @@ tree = Pouch.merge(tree, path).tree; | ||
var focusedInput; | ||
function input(text){ | ||
var div = document.createElement('div'); | ||
div.classList.add('input'); | ||
var span = document.createElement('span'); | ||
div.appendChild(span); | ||
span.appendChild(document.createTextNode(text)); | ||
var clicked = false; | ||
var input; | ||
div.ondblclick = function() { | ||
if(clicked){ | ||
input.focus(); | ||
return; | ||
} | ||
clicked = true; | ||
div.removeChild(span); | ||
input = document.createElement('input'); | ||
div.appendChild(input); | ||
input.value = text; | ||
input.focus(); | ||
input.onkeydown = function(e){ | ||
if(e.keyCode === 9 && !e.shiftKey){ | ||
var next; | ||
if(next = this.parentNode.parentNode.nextSibling){ | ||
next.firstChild.ondblclick(); | ||
e.preventDefault(); | ||
} | ||
} | ||
}; | ||
}; | ||
div.getValue = function() { | ||
return clicked ? input.value : text; | ||
}; | ||
return div; | ||
} | ||
function node(x, y, rev, isLeaf, isDeleted, isWinner, shortDescLen){ | ||
var nodeEl = circ(x, y, r, isLeaf, rev in deleted, rev === winner); | ||
var pos = rev.split('-')[0]; | ||
var id = rev.split('-')[1]; | ||
var opened = false; | ||
var click = function() { | ||
if (opened) return; | ||
opened = true; | ||
var div = document.createElement('div'); | ||
div.classList.add("editor"); | ||
div.classList.add("box"); | ||
div.style.left = scale * (x + 3 * r) + "px"; | ||
div.style.top = scale * (y - 2) + "px"; | ||
div.style.zIndex = 1000; | ||
box.appendChild(div); | ||
var close = function() { | ||
div.parentNode.removeChild(div); | ||
opened = false; | ||
}; | ||
db.get(docId, {rev: rev}, function(err, doc){ | ||
var dl = document.createElement('dl'); | ||
var keys = []; | ||
var addRow = function(key, value){ | ||
var key = input(key); | ||
keys.push(key); | ||
var dt = document.createElement('dt'); | ||
dt.appendChild(key); | ||
dl.appendChild(dt); | ||
var value = input(value); | ||
key.valueInput = value; | ||
var dd = document.createElement('dd'); | ||
dd.appendChild(value); | ||
dl.appendChild(dd); | ||
}; | ||
for (var i in doc) { | ||
if (doc.hasOwnProperty(i)) { | ||
addRow(i, JSON.stringify(doc[i])); | ||
} | ||
} | ||
div.appendChild(dl); | ||
var addButton = document.createElement('button'); | ||
addButton.appendChild(document.createTextNode('add')); | ||
div.appendChild(addButton); | ||
addButton.onclick = function(){ | ||
addRow('key', 'value'); | ||
}; | ||
var okButton = document.createElement('button'); | ||
okButton.appendChild(document.createTextNode('ok')); | ||
div.appendChild(okButton); | ||
okButton.onclick = function() { | ||
var newDoc = {}; | ||
keys.forEach(function(key){ | ||
var value = key.valueInput.getValue(); | ||
if (value.replace(/^\s*|\s*$/g, '')){ | ||
newDoc[key.getValue()] = JSON.parse(key.valueInput.getValue()); | ||
} | ||
}); | ||
putAfter(newDoc, doc._rev, function(err, ok){ | ||
if (!err) { | ||
close(); | ||
} else { | ||
console.error(err); | ||
alert("error occured, see console"); | ||
} | ||
}); | ||
}; | ||
var cancelButton = document.createElement('button'); | ||
cancelButton.appendChild(document.createTextNode('cancel')); | ||
div.appendChild(cancelButton); | ||
cancelButton.onclick = close; | ||
}); | ||
}; | ||
nodeEl.onclick = click; | ||
nodeEl.onmouseover = function() { | ||
this.classList.add("selected"); | ||
//text.style.display = "block"; | ||
}; | ||
nodeEl.onmouseout = function() { | ||
this.classList.remove("selected"); | ||
//text.style.display = "none"; | ||
}; | ||
var text = document.createElement('div'); | ||
//text.style.display = "none"; | ||
text.classList.add("box"); | ||
text.style.left = scale * (x + 3 * r) + "px"; | ||
text.style.top = scale * (y - 2) + "px"; | ||
text.short = pos + '-' + id.substr(0, shortDescLen); | ||
text.long = pos + '-' + id; | ||
text.appendChild(document.createTextNode(text.short)); | ||
text.onmouseover = function() { | ||
this.style.zIndex = 1000; | ||
}; | ||
text.onmouseout = function() { | ||
this.style.zIndex = 1; | ||
}; | ||
text.onclick = click; | ||
box.appendChild(text); | ||
} | ||
function draw(forest){ | ||
var grid = 10; | ||
var minUniq = minUniqueLength(allRevs); | ||
var maxX = grid; | ||
var maxY = grid; | ||
var r = 1; | ||
var levelCount = []; // numer of nodes on some level (pos) | ||
traverseRevTree(forest, function(isLeaf, pos, id, ctx) { | ||
Pouch.merge.traverseRevTree(forest, function(isLeaf, pos, id, ctx) { | ||
if (!levelCount[pos]) { | ||
@@ -134,13 +325,6 @@ levelCount[pos] = 1; | ||
var nodeEl = circ(x, y, r, isLeaf, rev in deleted, rev === winner); | ||
nodeEl.rev = rev; | ||
nodeEl.pos = pos; | ||
nodeEl.onclick = function() { | ||
var that = this; | ||
db.get(docId, {rev: this.rev}, function(err, doc){ | ||
console.log(that.rev, err, doc); | ||
}); | ||
}; | ||
node(x, y, rev, isLeaf, rev in deleted, rev === winner, minUniq); | ||
if (ctx) { | ||
line(x, y, ctx.x, ctx.y); | ||
line(x, y, ctx.x, ctx.y); | ||
} | ||
@@ -150,3 +334,5 @@ return {x: x, y: y}; | ||
svg.setAttribute('viewBox', '0 0 ' + (maxX + grid) + ' ' + (maxY + grid)); | ||
callback(null, svg); | ||
svg.style.width = scale * (maxX + grid) + 'px'; | ||
svg.style.height = scale * (maxY + grid) + 'px'; | ||
callback(null, box); | ||
} | ||
@@ -153,0 +339,0 @@ }; |
@@ -1,2 +0,4 @@ | ||
/*globals yankError: false, extend: false, call: false, parseDocId: false */ | ||
/*globals Pouch: true, yankError: false, extend: false, call: false, parseDocId: false, traverseRevTree: false */ | ||
/*globals arrayFirst: false, rootToLeaf: false, computeHeight: false */ | ||
/*globals cordova, isCordova */ | ||
@@ -9,119 +11,496 @@ "use strict"; | ||
var PouchAdapter = function(opts, callback) { | ||
var api = Pouch.adapters[opts.adapter](opts, callback); | ||
api.replicate = {}; | ||
var api = {}; | ||
if (!api.hasOwnProperty('post')) { | ||
api.post = function (doc, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
var customApi = Pouch.adapters[opts.adapter](opts, function(err, db) { | ||
if (err) { | ||
if (callback) { | ||
callback(err); | ||
} | ||
return api.bulkDocs({docs: [doc]}, opts, yankError(callback)); | ||
return; | ||
} | ||
for (var j in api) { | ||
if (!db.hasOwnProperty(j)) { | ||
db[j] = api[j]; | ||
} | ||
} | ||
// Don't call Pouch.open for ALL_DBS | ||
// Pouch.open saves the db's name into ALL_DBS | ||
if (opts.name === Pouch.prefix + Pouch.ALL_DBS) { | ||
callback(err, db); | ||
} else { | ||
Pouch.open(opts, function(err) { | ||
callback(err, db); | ||
}); | ||
} | ||
}); | ||
var auto_compaction = (opts.auto_compaction === true); | ||
// wraps a callback with a function that runs compaction after each edit | ||
var autoCompact = function(callback) { | ||
if (!auto_compaction) { | ||
return callback; | ||
} | ||
return function(err, res) { | ||
if (err) { | ||
call(callback, err); | ||
} else { | ||
var count = res.length; | ||
var decCount = function() { | ||
count--; | ||
if (!count) { | ||
call(callback, null, res); | ||
} | ||
}; | ||
res.forEach(function(doc) { | ||
if (doc.ok) { | ||
// TODO: we need better error handling | ||
compactDocument(doc.id, 1, decCount); | ||
} else { | ||
decCount(); | ||
} | ||
}); | ||
} | ||
}; | ||
} | ||
}; | ||
if (!api.hasOwnProperty('put')) { | ||
api.post = function (doc, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
return customApi.bulkDocs({docs: [doc]}, opts, | ||
autoCompact(yankError(callback))); | ||
}; | ||
api.put = function(doc, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
api.put = function(doc, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (!doc || !('_id' in doc)) { | ||
return call(callback, Pouch.Errors.MISSING_ID); | ||
} | ||
return customApi.bulkDocs({docs: [doc]}, opts, | ||
autoCompact(yankError(callback))); | ||
}; | ||
api.putAttachment = function (id, rev, blob, type, callback) { | ||
if (typeof type === 'function') { | ||
callback = type; | ||
type = blob; | ||
blob = rev; | ||
rev = null; | ||
} | ||
if (typeof type === 'undefined') { | ||
type = blob; | ||
blob = rev; | ||
rev = null; | ||
} | ||
id = parseDocId(id); | ||
function createAttachment(doc) { | ||
doc._attachments = doc._attachments || {}; | ||
doc._attachments[id.attachmentId] = { | ||
content_type: type, | ||
data: blob | ||
}; | ||
api.put(doc, callback); | ||
} | ||
api.get(id.docId, function(err, doc) { | ||
// create new doc | ||
if (err && err.error === Pouch.Errors.MISSING_DOC.error) { | ||
createAttachment({_id: id.docId}); | ||
return; | ||
} | ||
if (err) { | ||
call(callback, err); | ||
return; | ||
} | ||
if (!doc || !('_id' in doc)) { | ||
return call(callback, Pouch.Errors.MISSING_ID); | ||
if (doc._rev !== rev) { | ||
call(callback, Pouch.Errors.REV_CONFLICT); | ||
return; | ||
} | ||
return api.bulkDocs({docs: [doc]}, opts, yankError(callback)); | ||
}; | ||
} | ||
createAttachment(doc); | ||
}); | ||
}; | ||
if (!api.hasOwnProperty('putAttachment')) { | ||
api.putAttachment = api.putAttachment = function (id, rev, doc, type, callback) { | ||
id = parseDocId(id); | ||
api.get(id.docId, {attachments: true}, function(err, obj) { | ||
obj._attachments = obj._attachments || {}; | ||
obj._attachments[id.attachmentId] = { | ||
content_type: type, | ||
data: btoa(doc) | ||
}; | ||
api.put(obj, callback); | ||
api.removeAttachment = function (id, rev, callback) { | ||
id = parseDocId(id); | ||
api.get(id.docId, function(err, obj) { | ||
if (err) { | ||
call(callback, err); | ||
return; | ||
} | ||
if (obj._rev !== rev) { | ||
call(callback, Pouch.Errors.REV_CONFLICT); | ||
return; | ||
} | ||
obj._attachments = obj._attachments || {}; | ||
delete obj._attachments[id.attachmentId]; | ||
api.put(obj, callback); | ||
}); | ||
}; | ||
api.remove = function (doc, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (opts === undefined) { | ||
opts = {}; | ||
} | ||
opts.was_delete = true; | ||
var newDoc = {_id: doc._id, _rev: doc._rev}; | ||
newDoc._deleted = true; | ||
return customApi.bulkDocs({docs: [newDoc]}, opts, yankError(callback)); | ||
}; | ||
api.revsDiff = function (req, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
var ids = Object.keys(req); | ||
var count = 0; | ||
var missing = {}; | ||
function readDoc(err, doc, id) { | ||
req[id].map(function(revId) { | ||
var matches = function(x) { return x.rev !== revId; }; | ||
if (!doc || doc._revs_info.every(matches)) { | ||
if (!missing[id]) { | ||
missing[id] = {missing: []}; | ||
} | ||
missing[id].missing.push(revId); | ||
} | ||
}); | ||
}; | ||
} | ||
if (!api.hasOwnProperty('removeAttachment')) { | ||
api.removeAttachment = function (id, rev, callback) { | ||
id = parseDocId(id); | ||
api.get(id.docId, function(err, obj) { | ||
if (err) { | ||
call(callback, err); | ||
return; | ||
if (++count === ids.length) { | ||
return call(callback, null, missing); | ||
} | ||
} | ||
ids.map(function(id) { | ||
api.get(id, {revs_info: true}, function(err, doc) { | ||
readDoc(err, doc, id); | ||
}); | ||
}); | ||
}; | ||
// compact one document and fire callback | ||
// by compacting we mean removing all revisions which | ||
// are further from the leaf in revision tree than max_height | ||
var compactDocument = function(docId, max_height, callback) { | ||
customApi._getRevisionTree(docId, function(err, rev_tree){ | ||
if (err) { | ||
return call(callback); | ||
} | ||
var height = computeHeight(rev_tree); | ||
var candidates = []; | ||
var revs = []; | ||
Object.keys(height).forEach(function(rev) { | ||
if (height[rev] > max_height) { | ||
candidates.push(rev); | ||
} | ||
}); | ||
if (obj._rev !== rev) { | ||
call(callback, Pouch.Errors.REV_CONFLICT); | ||
return; | ||
Pouch.merge.traverseRevTree(rev_tree, function(isLeaf, pos, revHash, ctx, opts) { | ||
var rev = pos + '-' + revHash; | ||
if (opts.status === 'available' && candidates.indexOf(rev) !== -1) { | ||
opts.status = 'missing'; | ||
revs.push(rev); | ||
} | ||
obj._attachments = obj._attachments || {}; | ||
delete obj._attachments[id.attachmentId]; | ||
api.put(obj, callback); | ||
}); | ||
}; | ||
} | ||
customApi._doCompaction(docId, rev_tree, revs, callback); | ||
}); | ||
}; | ||
if (!api.hasOwnProperty('remove')) { | ||
api.remove = function (doc, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
// compact the whole database using single document | ||
// compaction | ||
api.compact = function(callback) { | ||
api.changes({complete: function(err, res) { | ||
if (err) { | ||
call(callback); // TODO: silently fail | ||
return; | ||
} | ||
if (opts === undefined) { | ||
opts = {}; | ||
var count = res.results.length; | ||
if (!count) { | ||
call(callback); | ||
return; | ||
} | ||
opts.was_delete = true; | ||
var newDoc = extend(true, {}, doc); | ||
newDoc._deleted = true; | ||
return api.bulkDocs({docs: [newDoc]}, opts, yankError(callback)); | ||
}; | ||
res.results.forEach(function(row) { | ||
compactDocument(row.id, 0, function() { | ||
count--; | ||
if (!count) { | ||
call(callback); | ||
} | ||
}); | ||
}); | ||
}}); | ||
}; | ||
} | ||
/* Begin api wrappers. Specific functionality to storage belongs in the _[method] */ | ||
api.get = function (id, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('get', arguments); | ||
return; | ||
} | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (!api.hasOwnProperty('revsDiff')) { | ||
api.revsDiff = function (req, opts, callback) { | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
var leaves = []; | ||
function finishOpenRevs() { | ||
var result = []; | ||
var count = leaves.length; | ||
if (!count) { | ||
return call(callback, null, result); | ||
} | ||
var ids = Object.keys(req); | ||
var count = 0; | ||
var missing = {}; | ||
// order with open_revs is unspecified | ||
leaves.forEach(function(leaf){ | ||
api.get(id, {rev: leaf, revs: opts.revs}, function(err, doc){ | ||
if (!err) { | ||
result.push({ok: doc}); | ||
} else { | ||
result.push({missing: leaf}); | ||
} | ||
count--; | ||
if(!count) { | ||
call(callback, null, result); | ||
} | ||
}); | ||
}); | ||
} | ||
function readDoc(err, doc, id) { | ||
req[id].map(function(revId) { | ||
var matches = function(x) { return x.rev !== revId; }; | ||
if (!doc || doc._revs_info.every(matches)) { | ||
if (!missing[id]) { | ||
missing[id] = {missing: []}; | ||
if (opts.open_revs) { | ||
if (opts.open_revs === "all") { | ||
customApi._getRevisionTree(id, function(err, rev_tree){ | ||
if (err) { | ||
// if there's no such document we should treat this | ||
// situation the same way as if revision tree was empty | ||
rev_tree = []; | ||
} | ||
leaves = Pouch.merge.collectLeaves(rev_tree).map(function(leaf){ | ||
return leaf.rev; | ||
}); | ||
finishOpenRevs(); | ||
}); | ||
} else { | ||
if (Array.isArray(opts.open_revs)) { | ||
leaves = opts.open_revs; | ||
for (var i = 0; i < leaves.length; i++) { | ||
var l = leaves[i]; | ||
// looks like it's the only thing couchdb checks | ||
if (!(typeof(l) === "string" && /^\d+-/.test(l))) { | ||
return call(callback, Pouch.error(Pouch.Errors.BAD_REQUEST, | ||
"Invalid rev format" )); | ||
} | ||
missing[id].missing.push(revId); | ||
} | ||
finishOpenRevs(); | ||
} else { | ||
return call(callback, Pouch.error(Pouch.Errors.UNKNOWN_ERROR, | ||
'function_clause')); | ||
} | ||
} | ||
return; // open_revs does not like other options | ||
} | ||
id = parseDocId(id); | ||
if (id.attachmentId !== '') { | ||
return customApi.getAttachment(id, callback); | ||
} | ||
return customApi._get(id, opts, function(result, metadata) { | ||
if ('error' in result) { | ||
return call(callback, result); | ||
} | ||
var doc = result; | ||
if (opts.conflicts) { | ||
var conflicts = Pouch.merge.collectConflicts(metadata); | ||
if (conflicts.length) { | ||
doc._conflicts = conflicts; | ||
} | ||
} | ||
if (opts.revs || opts.revs_info) { | ||
var paths = rootToLeaf(metadata.rev_tree); | ||
var path = arrayFirst(paths, function(arr) { | ||
return arr.ids.map(function(x) { return x.id; }) | ||
.indexOf(doc._rev.split('-')[1]) !== -1; | ||
}); | ||
if (++count === ids.length) { | ||
return call(callback, null, missing); | ||
path.ids.splice(path.ids.map(function(x) {return x.id;}) | ||
.indexOf(doc._rev.split('-')[1]) + 1); | ||
path.ids.reverse(); | ||
if (opts.revs) { | ||
doc._revisions = { | ||
start: (path.pos + path.ids.length) - 1, | ||
ids: path.ids.map(function(rev) { | ||
return rev.id; | ||
}) | ||
}; | ||
} | ||
if (opts.revs_info) { | ||
var pos = path.pos + path.ids.length; | ||
doc._revs_info = path.ids.map(function(rev) { | ||
pos--; | ||
return { | ||
rev: pos + '-' + rev.id, | ||
status: rev.opts.status | ||
}; | ||
}); | ||
} | ||
} | ||
call(callback, null, doc); | ||
}); | ||
}; | ||
ids.map(function(id) { | ||
api.get(id, {revs_info: true}, function(err, doc) { | ||
readDoc(err, doc, id); | ||
}); | ||
api.getAttachment = function(id, opts, callback) { | ||
if (opts instanceof Function) { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (typeof id === 'string') { | ||
id = parseDocId(id); | ||
} | ||
return customApi._getAttachment(id, opts, callback); | ||
}; | ||
api.allDocs = function(opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('allDocs', arguments); | ||
return; | ||
} | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if ('keys' in opts) { | ||
if ('startkey' in opts) { | ||
call(callback, Pouch.error(Pouch.Errors.QUERY_PARSE_ERROR, | ||
'Query parameter `start_key` is not compatible with multi-get' | ||
)); | ||
return; | ||
} | ||
if ('endkey' in opts) { | ||
call(callback, Pouch.error(Pouch.Errors.QUERY_PARSE_ERROR, | ||
'Query parameter `end_key` is not compatible with multi-get' | ||
)); | ||
return; | ||
} | ||
} | ||
return customApi._allDocs(opts, callback); | ||
}; | ||
api.changes = function(opts) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('changes', arguments); | ||
return; | ||
} | ||
opts = extend(true, {}, opts); | ||
// 0 and 1 should return 1 document | ||
opts.limit = opts.limit === 0 ? 1 : opts.limit; | ||
return customApi._changes(opts); | ||
}; | ||
api.close = function(callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('close', arguments); | ||
return; | ||
} | ||
return customApi._close(callback); | ||
}; | ||
api.info = function(callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('info', arguments); | ||
return; | ||
} | ||
return customApi._info(callback); | ||
}; | ||
api.id = function() { | ||
return customApi._id(); | ||
}; | ||
api.type = function() { | ||
return (typeof customApi._type === 'function') ? customApi._type() : opts.adapter; | ||
}; | ||
api.bulkDocs = function(req, opts, callback) { | ||
if (!api.taskqueue.ready()) { | ||
api.taskqueue.addTask('bulkDocs', arguments); | ||
return; | ||
} | ||
if (typeof opts === 'function') { | ||
callback = opts; | ||
opts = {}; | ||
} | ||
if (!opts) { | ||
opts = {}; | ||
} else { | ||
opts = extend(true, {}, opts); | ||
} | ||
if (!req || !req.docs || req.docs.length < 1) { | ||
return call(callback, Pouch.Errors.MISSING_BULK_DOCS); | ||
} | ||
if (!Array.isArray(req.docs)) { | ||
return call(callback, Pouch.Errors.QUERY_PARSE_ERROR); | ||
} | ||
req = extend(true, {}, req); | ||
if (!('new_edits' in opts)) { | ||
opts.new_edits = true; | ||
} | ||
return customApi._bulkDocs(req, opts, autoCompact(callback)); | ||
}; | ||
/* End Wrappers */ | ||
var taskqueue = {}; | ||
taskqueue.ready = false; | ||
taskqueue.queue = []; | ||
api.taskqueue = {}; | ||
api.taskqueue.execute = function (db) { | ||
if (taskqueue.ready) { | ||
taskqueue.queue.forEach(function(d) { | ||
db[d.task].apply(null, d.parameters); | ||
}); | ||
}; | ||
} | ||
}; | ||
} | ||
api.taskqueue.ready = function() { | ||
if (arguments.length === 0) { | ||
return taskqueue.ready; | ||
} | ||
taskqueue.ready = arguments[0]; | ||
}; | ||
api.taskqueue.addTask = function(task, parameters) { | ||
taskqueue.queue.push({ task: task, parameters: parameters }); | ||
}; | ||
api.replicate = {}; | ||
api.replicate.from = function (url, opts, callback) { | ||
@@ -132,3 +511,3 @@ if (typeof opts === 'function') { | ||
} | ||
return Pouch.replicate(url, api, opts, callback); | ||
return Pouch.replicate(url, customApi, opts, callback); | ||
}; | ||
@@ -141,6 +520,22 @@ | ||
} | ||
return Pouch.replicate(api, dbName, opts, callback); | ||
return Pouch.replicate(customApi, dbName, opts, callback); | ||
}; | ||
return api; | ||
for (var j in api) { | ||
if (!customApi.hasOwnProperty(j)) { | ||
customApi[j] = api[j]; | ||
} | ||
} | ||
// Http adapter can skip setup so we force the db to be ready and execute any jobs | ||
if (opts.skipSetup) { | ||
api.taskqueue.ready(true); | ||
api.taskqueue.execute(api); | ||
} | ||
if (isCordova()){ | ||
//to inform websql adapter that we can use api | ||
cordova.fireWindowEvent(opts.name + "_pouch", {}); | ||
} | ||
return customApi; | ||
}; | ||
@@ -147,0 +542,0 @@ |
237
src/pouch.js
@@ -1,2 +0,2 @@ | ||
/*globals PouchAdapter: true */ | ||
/*globals PouchAdapter: true, extend: true */ | ||
@@ -21,3 +21,8 @@ "use strict"; | ||
if (typeof callback === 'undefined') { | ||
callback = function() {}; | ||
} | ||
var backend = Pouch.parseAdapter(opts.name || name); | ||
opts.originalName = name; | ||
opts.name = opts.name || backend.name; | ||
@@ -41,2 +46,3 @@ opts.adapter = opts.adapter || backend.adapter; | ||
} | ||
for (var plugin in Pouch.plugins) { | ||
@@ -54,8 +60,21 @@ // In future these will likely need to be async to allow the plugin | ||
} | ||
db.taskqueue.ready(true); | ||
db.taskqueue.execute(db); | ||
callback(null, db); | ||
}); | ||
for (var j in adapter) { | ||
this[j] = adapter[j]; | ||
} | ||
for (var plugin in Pouch.plugins) { | ||
// In future these will likely need to be async to allow the plugin | ||
// to initialise | ||
var pluginObj = Pouch.plugins[plugin](this); | ||
for (var api in pluginObj) { | ||
// We let things like the http adapter use its own implementation | ||
// as it shares a lot of code | ||
if (!(api in this)) { | ||
this[api] = pluginObj[api]; | ||
} | ||
} | ||
} | ||
}; | ||
@@ -68,4 +87,5 @@ | ||
Pouch.prefix = '_pouch_'; | ||
Pouch.parseAdapter = function(name) { | ||
var match = name.match(/([a-z\-]*):\/\/(.*)/); | ||
@@ -82,20 +102,76 @@ if (match) { | ||
var rank = {'idb': 1, 'leveldb': 2, 'websql': 3, 'http': 4, 'https': 4}; | ||
var rankedAdapter = Object.keys(Pouch.adapters).sort(function(a, b) { | ||
return rank[a] - rank[b]; | ||
})[0]; | ||
var preferredAdapters = ['idb', 'leveldb', 'websql']; | ||
for (var i = 0; i < preferredAdapters.length; ++i) { | ||
if (preferredAdapters[i] in Pouch.adapters) { | ||
return { | ||
name: Pouch.prefix + name, | ||
adapter: preferredAdapters[i] | ||
}; | ||
} | ||
} | ||
return { | ||
name: name, | ||
adapter: rankedAdapter | ||
throw 'No valid adapter found'; | ||
}; | ||
Pouch.destroy = function(name, callback) { | ||
var opts = Pouch.parseAdapter(name); | ||
var cb = function(err, response) { | ||
if (err) { | ||
callback(err); | ||
return; | ||
} | ||
for (var plugin in Pouch.plugins) { | ||
Pouch.plugins[plugin]._delete(name); | ||
} | ||
if (Pouch.DEBUG) { | ||
console.log(name + ': Delete Database'); | ||
} | ||
// call destroy method of the particular adaptor | ||
Pouch.adapters[opts.adapter].destroy(opts.name, callback); | ||
}; | ||
// remove Pouch from allDBs | ||
Pouch.removeFromAllDbs(opts, cb); | ||
}; | ||
Pouch.removeFromAllDbs = function(opts, callback) { | ||
// Only execute function if flag is enabled | ||
if (!Pouch.enableAllDbs) { | ||
callback(); | ||
return; | ||
} | ||
Pouch.destroy = function(name, callback) { | ||
for (var plugin in Pouch.plugins) { | ||
Pouch.plugins[plugin]._delete(name); | ||
// skip http and https adaptors for allDbs | ||
var adapter = opts.adapter; | ||
if (adapter === "http" || adapter === "https") { | ||
callback(); | ||
return; | ||
} | ||
var opts = Pouch.parseAdapter(name); | ||
Pouch.adapters[opts.adapter].destroy(opts.name, callback); | ||
// remove db from Pouch.ALL_DBS | ||
new Pouch(Pouch.allDBName(opts.adapter), function(err, db) { | ||
if (err) { | ||
// don't fail when allDbs fail | ||
console.error(err); | ||
callback(); | ||
return; | ||
} | ||
// check if db has been registered in Pouch.ALL_DBS | ||
var dbname = Pouch.dbName(opts.adapter, opts.name); | ||
db.get(dbname, function(err, doc) { | ||
if (err) { | ||
callback(); | ||
} else { | ||
db.remove(doc, function(err, response) { | ||
if (err) { | ||
console.error(err); | ||
} | ||
callback(); | ||
}); | ||
} | ||
}); | ||
}); | ||
}; | ||
@@ -113,2 +189,120 @@ | ||
// flag to toggle allDbs (off by default) | ||
Pouch.enableAllDbs = false; | ||
// name of database used to keep track of databases | ||
Pouch.ALL_DBS = "_allDbs"; | ||
Pouch.dbName = function(adapter, name) { | ||
return [adapter, "-", name].join(''); | ||
}; | ||
Pouch.realDBName = function(adapter, name) { | ||
return [adapter, "://", name].join(''); | ||
}; | ||
Pouch.allDBName = function(adapter) { | ||
return [adapter, "://", Pouch.prefix + Pouch.ALL_DBS].join(''); | ||
}; | ||
Pouch.open = function(opts, callback) { | ||
// Only register pouch with allDbs if flag is enabled | ||
if (!Pouch.enableAllDbs) { | ||
callback(); | ||
return; | ||
} | ||
var adapter = opts.adapter; | ||
// skip http and https adaptors for allDbs | ||
if (adapter === "http" || adapter === "https") { | ||
callback(); | ||
return; | ||
} | ||
new Pouch(Pouch.allDBName(adapter), function(err, db) { | ||
if (err) { | ||
// don't fail when allDb registration fails | ||
console.error(err); | ||
callback(); | ||
return; | ||
} | ||
// check if db has been registered in Pouch.ALL_DBS | ||
var dbname = Pouch.dbName(adapter, opts.name); | ||
db.get(dbname, function(err, response) { | ||
if (err && err.status === 404) { | ||
db.put({ | ||
_id: dbname, | ||
dbname: opts.originalName | ||
}, function(err) { | ||
if (err) { | ||
console.error(err); | ||
} | ||
callback(); | ||
}); | ||
} else { | ||
callback(); | ||
} | ||
}); | ||
}); | ||
}; | ||
Pouch.allDbs = function(callback) { | ||
var accumulate = function(adapters, all_dbs) { | ||
if (adapters.length === 0) { | ||
// remove duplicates | ||
var result = []; | ||
all_dbs.forEach(function(doc) { | ||
var exists = result.some(function(db) { | ||
return db.id === doc.id; | ||
}); | ||
if (!exists) { | ||
result.push(doc); | ||
} | ||
}); | ||
// return an array of dbname | ||
callback(null, result.map(function(row) { | ||
return row.doc.dbname; | ||
})); | ||
return; | ||
} | ||
var adapter = adapters.shift(); | ||
// skip http and https adaptors for allDbs | ||
if (adapter === "http" || adapter === "https") { | ||
accumulate(adapters, all_dbs); | ||
return; | ||
} | ||
new Pouch(Pouch.allDBName(adapter), function(err, db) { | ||
if (err) { | ||
callback(err); | ||
return; | ||
} | ||
db.allDocs({include_docs: true}, function(err, response) { | ||
if (err) { | ||
callback(err); | ||
return; | ||
} | ||
// append from current adapter rows | ||
all_dbs.unshift.apply(all_dbs, response.rows); | ||
// code to clear allDbs. | ||
// response.rows.forEach(function(row) { | ||
// db.remove(row.doc, function() { | ||
// console.log(arguments); | ||
// }); | ||
// }); | ||
// recurse | ||
accumulate(adapters, all_dbs); | ||
}); | ||
}); | ||
}; | ||
var adapters = Object.keys(Pouch.adapters); | ||
accumulate(adapters, []); | ||
}; | ||
// Enumerate errors, add the status code so we can reflect the HTTP api | ||
@@ -157,2 +351,7 @@ // in future | ||
}, | ||
BAD_ARG: { | ||
status: 500, | ||
error: 'badarg', | ||
reason: 'Some query argument is invalid' | ||
}, | ||
INVALID_REQUEST: { | ||
@@ -174,3 +373,5 @@ status: 400, | ||
}; | ||
Pouch.error = function(error, reason){ | ||
return extend({}, error, {reason: reason}); | ||
}; | ||
if (typeof module !== 'undefined' && module.exports) { | ||
@@ -182,6 +383,6 @@ global.Pouch = Pouch; | ||
Pouch.utils = require('./pouch.utils.js'); | ||
extend = Pouch.utils.extend; | ||
module.exports = Pouch; | ||
var PouchAdapter = require('./pouch.adapter.js'); | ||
// load adapters known to work under node | ||
//load adapters known to work under node | ||
var adapters = ['leveldb', 'http']; | ||
@@ -188,0 +389,0 @@ adapters.map(function(adapter) { |
@@ -1,2 +0,2 @@ | ||
/*globals rootToLeaf: false, extend: false, traverseRevTree: false */ | ||
/*globals rootToLeaf: false, extend: false */ | ||
@@ -23,7 +23,8 @@ 'use strict'; | ||
// Path = {pos: position_from_root, ids: Tree} | ||
// Tree = [Key, [Tree, ...]], in particular single node: [Key, []] | ||
// Tree = [Key, Opts, [Tree, ...]], in particular single node: [Key, []] | ||
// Turn a path as a flat array into a tree with a single branch | ||
function pathToTree(path) { | ||
var root = [path.shift(), []]; | ||
var doc = path.shift(); | ||
var root = [doc.id, doc.opts, []]; | ||
var leaf = root; | ||
@@ -33,4 +34,5 @@ var nleaf; | ||
while (path.length) { | ||
nleaf = [path.shift(), []]; | ||
leaf[1].push(nleaf); | ||
doc = path.shift(); | ||
nleaf = [doc.id, doc.opts, []]; | ||
leaf[2].push(nleaf); | ||
leaf = nleaf; | ||
@@ -51,6 +53,11 @@ } | ||
for (var i = 0; i < tree2[1].length; i++) { | ||
if (!tree1[1][0]) { | ||
if (tree1[1].status || tree2[1].status) { | ||
tree1[1].status = (tree1[1].status === 'available' || | ||
tree2[1].status === 'available') ? 'available' : 'missing'; | ||
} | ||
for (var i = 0; i < tree2[2].length; i++) { | ||
if (!tree1[2][0]) { | ||
conflicts = 'new_leaf'; | ||
tree1[1][0] = tree2[1][i]; | ||
tree1[2][0] = tree2[2][i]; | ||
continue; | ||
@@ -60,5 +67,5 @@ } | ||
var merged = false; | ||
for (var j = 0; j < tree1[1].length; j++) { | ||
if (tree1[1][j][0] === tree2[1][i][0]) { | ||
queue.push({tree1: tree1[1][j], tree2: tree2[1][i]}); | ||
for (var j = 0; j < tree1[2].length; j++) { | ||
if (tree1[2][j][0] === tree2[2][i][0]) { | ||
queue.push({tree1: tree1[2][j], tree2: tree2[2][i]}); | ||
merged = true; | ||
@@ -69,4 +76,4 @@ } | ||
conflicts = 'new_branch'; | ||
tree1[1].push(tree2[1][i]); | ||
tree1[1].sort(); | ||
tree1[2].push(tree2[2][i]); | ||
tree1[2].sort(); | ||
} | ||
@@ -122,3 +129,3 @@ } | ||
/*jshint loopfunc:true */ | ||
item.ids[1].forEach(function(el, idx) { | ||
item.ids[2].forEach(function(el, idx) { | ||
trees.push({ids: el, diff: item.diff-1, parent: item.ids, parentIdx: idx}); | ||
@@ -134,3 +141,3 @@ }); | ||
res = mergeTree(el.ids, t2.ids); | ||
el.parent[1][el.parentIdx] = res.tree; | ||
el.parent[2][el.parentIdx] = res.tree; | ||
restree.push({pos: t1.pos, ids: t1.ids}); | ||
@@ -195,15 +202,9 @@ conflicts = conflicts || res.conflicts; | ||
Pouch.merge.winningRev = function(metadata) { | ||
var deletions = metadata.deletions || {}; | ||
var leafs = []; | ||
traverseRevTree(metadata.rev_tree, function(isLeaf, pos, id) { | ||
Pouch.merge.traverseRevTree(metadata.rev_tree, | ||
function(isLeaf, pos, id, something, opts) { | ||
if (isLeaf) { | ||
leafs.push({pos: pos, id: id}); | ||
leafs.push({pos: pos, id: id, deleted: !!opts.deleted}); | ||
} | ||
}); | ||
leafs.forEach(function(leaf) { | ||
leaf.deleted = leaf.id in deletions; | ||
}); | ||
leafs.sort(function(a, b) { | ||
@@ -218,4 +219,57 @@ if (a.deleted !== b.deleted) { | ||
}); | ||
return leafs[0].pos + '-' + leafs[0].id; | ||
}; | ||
// Pretty much all below can be combined into a higher order function to | ||
// traverse revisions | ||
// Callback has signature function(isLeaf, pos, id, [context]) | ||
// The return value from the callback will be passed as context to all | ||
// children of that node | ||
Pouch.merge.traverseRevTree = function(revs, callback) { | ||
var toVisit = []; | ||
revs.forEach(function(tree) { | ||
toVisit.push({pos: tree.pos, ids: tree.ids}); | ||
}); | ||
while (toVisit.length > 0) { | ||
var node = toVisit.pop(); | ||
var pos = node.pos; | ||
var tree = node.ids; | ||
var newCtx = callback(tree[2].length === 0, pos, tree[0], node.ctx, tree[1]); | ||
/*jshint loopfunc: true */ | ||
tree[2].forEach(function(branch) { | ||
toVisit.push({pos: pos+1, ids: branch, ctx: newCtx}); | ||
}); | ||
} | ||
}; | ||
Pouch.merge.collectLeaves = function(revs) { | ||
var leaves = []; | ||
Pouch.merge.traverseRevTree(revs, function(isLeaf, pos, id, acc, opts) { | ||
if (isLeaf) { | ||
leaves.unshift({rev: pos + "-" + id, pos: pos, opts: opts}); | ||
} | ||
}); | ||
leaves.sort(function(a, b) { | ||
return b.pos - a.pos; | ||
}); | ||
leaves.map(function(leaf) { delete leaf.pos; }); | ||
return leaves; | ||
}; | ||
// returns all conflicts that is leaves such that | ||
// 1. are not deleted and | ||
// 2. are different than winning revision | ||
Pouch.merge.collectConflicts = function(metadata) { | ||
var win = Pouch.merge.winningRev(metadata); | ||
var leaves = Pouch.merge.collectLeaves(metadata.rev_tree); | ||
var conflicts = []; | ||
leaves.forEach(function(leaf) { | ||
if (leaf.rev !== win && !leaf.opts.deleted) { | ||
conflicts.push(leaf.rev); | ||
} | ||
}); | ||
return conflicts; | ||
}; | ||
@@ -1,2 +0,2 @@ | ||
/*globals fetchCheckpoint: false, writeCheckpoint: false, call: false */ | ||
/*globals call: false, Crypto: false*/ | ||
@@ -9,75 +9,208 @@ 'use strict'; | ||
function replicate(src, target, opts, callback, replicateRet) { | ||
// We create a basic promise so the caller can cancel the replication possibly | ||
// before we have actually started listening to changes etc | ||
var Promise = function() { | ||
this.cancelled = false; | ||
this.cancel = function() { | ||
this.cancelled = true; | ||
}; | ||
}; | ||
fetchCheckpoint(src, target, opts, function(checkpoint) { | ||
var results = []; | ||
var completed = false; | ||
var pending = 0; | ||
var last_seq = checkpoint; | ||
var continuous = opts.continuous || false; | ||
var result = { | ||
ok: true, | ||
start_time: new Date(), | ||
docs_read: 0, | ||
docs_written: 0 | ||
}; | ||
// The RequestManager ensures that only one database request is active at | ||
// at time, it ensures we dont max out simultaneous HTTP requests and makes | ||
// the replication process easier to reason about | ||
var RequestManager = function() { | ||
function isCompleted() { | ||
if (completed && pending === 0) { | ||
result.end_time = new Date(); | ||
writeCheckpoint(src, target, opts, last_seq, function() { | ||
call(callback, null, result); | ||
}); | ||
var queue = []; | ||
var api = {}; | ||
var processing = false; | ||
// Add a new request to the queue, if we arent currently processing anything | ||
// then process it immediately | ||
api.enqueue = function(fun, args) { | ||
queue.push({fun: fun, args: args}); | ||
if (!processing) { | ||
api.process(); | ||
} | ||
}; | ||
// Process the next request | ||
api.process = function() { | ||
if (processing || !queue.length) { | ||
return; | ||
} | ||
processing = true; | ||
var task = queue.shift(); | ||
task.fun.apply(null, task.args); | ||
}; | ||
// We need to be notified whenever a request is complete to process | ||
// the next request | ||
api.notifyRequestComplete = function() { | ||
processing = false; | ||
api.process(); | ||
}; | ||
return api; | ||
}; | ||
// TODO: check CouchDB's replication id generation, generate a unique id particular | ||
// to this replication | ||
var genReplicationId = function(src, target, opts) { | ||
var filterFun = opts.filter ? opts.filter.toString() : ''; | ||
return '_local/' + Crypto.MD5(src.id() + target.id() + filterFun); | ||
}; | ||
// A checkpoint lets us restart replications from when they were last cancelled | ||
var fetchCheckpoint = function(target, id, callback) { | ||
target.get(id, function(err, doc) { | ||
if (err && err.status === 404) { | ||
callback(null, 0); | ||
} else { | ||
callback(null, doc.last_seq); | ||
} | ||
}); | ||
}; | ||
var writeCheckpoint = function(target, id, checkpoint, callback) { | ||
var check = { | ||
_id: id, | ||
last_seq: checkpoint | ||
}; | ||
target.get(check._id, function(err, doc) { | ||
if (doc && doc._rev) { | ||
check._rev = doc._rev; | ||
} | ||
target.put(check, function(err, doc) { | ||
callback(); | ||
}); | ||
}); | ||
}; | ||
function replicate(src, target, opts, promise) { | ||
var requests = new RequestManager(); | ||
var writeQueue = []; | ||
var repId = genReplicationId(src, target, opts); | ||
var results = []; | ||
var completed = false; | ||
var pending = 0; | ||
var last_seq = 0; | ||
var continuous = opts.continuous || false; | ||
var doc_ids = opts.doc_ids; | ||
var result = { | ||
ok: true, | ||
start_time: new Date(), | ||
docs_read: 0, | ||
docs_written: 0 | ||
}; | ||
function docsWritten(err, res, len) { | ||
requests.notifyRequestComplete(); | ||
if (opts.onChange) { | ||
for (var i = 0; i < len; i++) { | ||
/*jshint validthis:true */ | ||
opts.onChange.apply(this, [result]); | ||
} | ||
} | ||
pending -= len; | ||
result.docs_written += len; | ||
isCompleted(); | ||
} | ||
if (replicateRet.cancelled) { | ||
function writeDocs() { | ||
if (!writeQueue.length) { | ||
return requests.notifyRequestComplete(); | ||
} | ||
var len = writeQueue.length; | ||
target.bulkDocs({docs: writeQueue}, {new_edits: false}, function(err, res) { | ||
docsWritten(err, res, len); | ||
}); | ||
writeQueue = []; | ||
} | ||
function eachRev(id, rev) { | ||
src.get(id, {revs: true, rev: rev, attachments: true}, function(err, doc) { | ||
requests.notifyRequestComplete(); | ||
writeQueue.push(doc); | ||
requests.enqueue(writeDocs); | ||
}); | ||
} | ||
function onRevsDiff(err, diffs) { | ||
requests.notifyRequestComplete(); | ||
if (err) { | ||
if (continuous) { | ||
promise.cancel(); | ||
} | ||
call(opts.complete, err, null); | ||
return; | ||
} | ||
// We already have the full document stored | ||
if (Object.keys(diffs).length === 0) { | ||
pending--; | ||
isCompleted(); | ||
return; | ||
} | ||
var _enqueuer = function (rev) { | ||
requests.enqueue(eachRev, [id, rev]); | ||
}; | ||
for (var id in diffs) { | ||
diffs[id].missing.forEach(_enqueuer); | ||
} | ||
} | ||
function fetchRevsDiff(diff) { | ||
target.revsDiff(diff, onRevsDiff); | ||
} | ||
function onChange(change) { | ||
last_seq = change.seq; | ||
results.push(change); | ||
result.docs_read++; | ||
pending++; | ||
var diff = {}; | ||
diff[change.id] = change.changes.map(function(x) { return x.rev; }); | ||
requests.enqueue(fetchRevsDiff, [diff]); | ||
} | ||
function complete() { | ||
completed = true; | ||
isCompleted(); | ||
} | ||
function isCompleted() { | ||
if (completed && pending === 0) { | ||
result.end_time = Date.now(); | ||
writeCheckpoint(target, repId, last_seq, function(err, res) { | ||
call(opts.complete, err, result); | ||
}); | ||
} | ||
} | ||
fetchCheckpoint(target, repId, function(err, checkpoint) { | ||
if (err) { | ||
return call(opts.complete, err); | ||
} | ||
last_seq = checkpoint; | ||
// Was the replication cancelled by the caller before it had a chance | ||
// to start. Shouldnt we be calling complete? | ||
if (promise.cancelled) { | ||
return; | ||
} | ||
var repOpts = { | ||
limit: 25, | ||
continuous: continuous, | ||
since: checkpoint, | ||
since: last_seq, | ||
style: 'all_docs', | ||
onChange: function(change) { | ||
last_seq = change.seq; | ||
results.push(change); | ||
result.docs_read++; | ||
pending++; | ||
var diff = {}; | ||
diff[change.id] = change.changes.map(function(x) { return x.rev; }); | ||
target.revsDiff(diff, function(err, diffs) { | ||
if (err) { | ||
if (continuous) { | ||
replicateRet.cancel(); | ||
} | ||
call(callback, err, null); | ||
return; | ||
} | ||
if (Object.keys(diffs).length === 0) { | ||
pending--; | ||
isCompleted(); | ||
return; | ||
} | ||
for (var id in diffs) { | ||
/*jshint loopfunc: true */ | ||
diffs[id].missing.map(function(rev) { | ||
src.get(id, {revs: true, rev: rev, attachments: true}, function(err, doc) { | ||
target.bulkDocs({docs: [doc]}, {new_edits: false}, function() { | ||
if (opts.onChange) { | ||
opts.onChange.apply(this, [result]); | ||
} | ||
result.docs_written++; | ||
pending--; | ||
isCompleted(); | ||
}); | ||
}); | ||
}); | ||
} | ||
}); | ||
}, | ||
complete: function(err, res) { | ||
completed = true; | ||
isCompleted(); | ||
} | ||
onChange: onChange, | ||
complete: complete, | ||
doc_ids: doc_ids | ||
}; | ||
@@ -94,6 +227,8 @@ | ||
var changes = src.changes(repOpts); | ||
if (opts.continuous) { | ||
replicateRet.cancel = changes.cancel; | ||
promise.cancel = changes.cancel; | ||
} | ||
}); | ||
} | ||
@@ -109,6 +244,2 @@ | ||
Pouch.replicate = function(src, target, opts, callback) { | ||
// TODO: This needs some cleaning up, from the replicate call I want | ||
// to return a promise in which I can cancel continuous replications | ||
// this will just proxy requests to cancel the changes feed but only | ||
// after we start actually running the changes feed | ||
if (opts instanceof Function) { | ||
@@ -121,10 +252,4 @@ callback = opts; | ||
} | ||
var Ret = function() { | ||
this.cancelled = false; | ||
this.cancel = function() { | ||
this.cancelled = true; | ||
}; | ||
}; | ||
var replicateRet = new Ret(); | ||
opts.complete = callback; | ||
var replicateRet = new Promise(); | ||
toPouch(src, function(err, src) { | ||
@@ -138,3 +263,3 @@ if (err) { | ||
} | ||
replicate(src, target, opts, callback, replicateRet); | ||
replicate(src, target, opts, replicateRet); | ||
}); | ||
@@ -141,0 +266,0 @@ }); |
/*jshint strict: false */ | ||
/*global request: true, Buffer: true, escape: true, $:true */ | ||
/*global extend: true, Crypto: true */ | ||
/*global chrome*/ | ||
@@ -47,19 +48,2 @@ // Pretty dumb name for a function, just wraps callback calls so we dont | ||
// check if a specific revision of a doc has been deleted | ||
// - metadata: the metadata object from the doc store | ||
// - rev: (optional) the revision to check. defaults to metadata.rev | ||
var isDeleted = function(metadata, rev) { | ||
if (!metadata || !metadata.deletions) { | ||
return false; | ||
} | ||
if (!rev) { | ||
rev = Pouch.merge.winningRev(metadata); | ||
} | ||
if (rev.indexOf('-') >= 0) { | ||
rev = rev.split('-')[1]; | ||
} | ||
return metadata.deletions[rev] === true; | ||
}; | ||
// Determine id an ID is valid | ||
@@ -99,2 +83,6 @@ // - invalid IDs begin with an underescore that does not begin '_design' or '_local' | ||
var revInfo; | ||
var opts = {status: 'available'}; | ||
if (doc._deleted) { | ||
opts.deleted = true; | ||
} | ||
@@ -113,3 +101,3 @@ if (newEdits) { | ||
pos: parseInt(revInfo[1], 10), | ||
ids: [revInfo[2], [[newRevId, []]]] | ||
ids: [revInfo[2], {status: 'missing'}, [[newRevId, opts, []]]] | ||
}]; | ||
@@ -120,3 +108,3 @@ nRevNum = parseInt(revInfo[1], 10) + 1; | ||
pos: 1, | ||
ids : [newRevId, []] | ||
ids : [newRevId, opts, []] | ||
}]; | ||
@@ -131,5 +119,5 @@ nRevNum = 1; | ||
if (acc === null) { | ||
return [x, []]; | ||
return [x, opts, []]; | ||
} else { | ||
return [x, [acc]]; | ||
return [x, {status: 'missing'}, [acc]]; | ||
} | ||
@@ -147,3 +135,3 @@ }, null) | ||
pos: parseInt(revInfo[1], 10), | ||
ids: [revInfo[2], []] | ||
ids: [revInfo[2], opts, []] | ||
}]; | ||
@@ -194,101 +182,30 @@ } | ||
// Pretty much all below can be combined into a higher order function to | ||
// traverse revisions | ||
// Callback has signature function(isLeaf, pos, id, [context]) | ||
// The return value from the callback will be passed as context to all children of that node | ||
var traverseRevTree = function(revs, callback) { | ||
var toVisit = []; | ||
revs.forEach(function(tree) { | ||
toVisit.push({pos: tree.pos, ids: tree.ids}); | ||
}); | ||
while (toVisit.length > 0) { | ||
var node = toVisit.pop(); | ||
var pos = node.pos; | ||
var tree = node.ids; | ||
var newCtx = callback(tree[1].length === 0, pos, tree[0], node.ctx); | ||
/*jshint loopfunc: true */ | ||
tree[1].forEach(function(branch) { | ||
toVisit.push({pos: pos+1, ids: branch, ctx: newCtx}); | ||
}); | ||
} | ||
}; | ||
var collectRevs = function(path) { | ||
var revs = []; | ||
traverseRevTree([path], function(isLeaf, pos, id) { | ||
revs.push({rev: pos + "-" + id, status: 'available'}); | ||
}); | ||
return revs; | ||
}; | ||
var collectLeaves = function(revs) { | ||
var leaves = []; | ||
traverseRevTree(revs, function(isLeaf, pos, id) { | ||
// for every node in a revision tree computes its distance from the closest | ||
// leaf | ||
var computeHeight = function(revs) { | ||
var height = {}; | ||
var edges = []; | ||
Pouch.merge.traverseRevTree(revs, function(isLeaf, pos, id, prnt) { | ||
var rev = pos + "-" + id; | ||
if (isLeaf) { | ||
leaves.unshift({rev: pos + "-" + id, pos: pos}); | ||
height[rev] = 0; | ||
} | ||
if (prnt !== undefined) { | ||
edges.push({from: prnt, to: rev}); | ||
} | ||
return rev; | ||
}); | ||
leaves.sort(function(a, b) { | ||
return b.pos - a.pos; | ||
}); | ||
leaves.map(function(leaf) { delete leaf.pos; }); | ||
return leaves; | ||
}; | ||
var collectConflicts = function(revs, deletions) { | ||
// Remove all deleted leaves | ||
var leaves = collectLeaves(revs); | ||
for(var i = 0; i < leaves.length; i++){ | ||
var leaf = leaves.shift(); | ||
var rev = leaf.rev.split("-")[1]; | ||
if(deletions && !deletions[rev]){ | ||
leaves.push(leaf); | ||
} | ||
} | ||
// First is current rev | ||
leaves.shift(); | ||
return leaves.map(function(x) { return x.rev; }); | ||
}; | ||
var fetchCheckpoint = function(src, target, opts, callback) { | ||
var filter_func = ''; | ||
if (typeof opts.filter !== "undefined") { | ||
filter_func = opts.filter.toString(); | ||
} | ||
var id = Crypto.MD5(src.id() + target.id() + filter_func); | ||
src.get('_local/' + id, function(err, doc) { | ||
if (err && err.status === 404) { | ||
callback(0); | ||
edges.reverse(); | ||
edges.forEach(function(edge) { | ||
if (height[edge.from] === undefined) { | ||
height[edge.from] = 1 + height[edge.to]; | ||
} else { | ||
callback(doc.last_seq); | ||
height[edge.from] = Math.min(height[edge.from], 1 + height[edge.to]); | ||
} | ||
}); | ||
return height; | ||
}; | ||
var writeCheckpoint = function(src, target, opts, checkpoint, callback) { | ||
var filter_func = ''; | ||
if (typeof opts.filter !== "undefined") { | ||
filter_func = opts.filter.toString(); | ||
} | ||
var check = { | ||
_id: '_local/' + Crypto.MD5(src.id() + target.id() + filter_func), | ||
last_seq: checkpoint | ||
}; | ||
src.get(check._id, function(err, doc) { | ||
if (doc && doc._rev) { | ||
check._rev = doc._rev; | ||
} | ||
src.put(check, function(err, doc) { | ||
callback(); | ||
}); | ||
}); | ||
}; | ||
// returns first element of arr satisfying callback predicate | ||
@@ -306,5 +223,10 @@ var arrayFirst = function(arr, callback) { | ||
return function(change) { | ||
if (opts.filter && !opts.filter.call(this, change.doc)) { | ||
var req = {}; | ||
req.query = opts.query_params; | ||
if (opts.filter && !opts.filter.call(this, change.doc, req)) { | ||
return; | ||
} | ||
if (opts.doc_ids && opts.doc_ids.indexOf(change.id) !== -1) { | ||
return; | ||
} | ||
if (!opts.include_docs) { | ||
@@ -321,5 +243,5 @@ delete change.doc; | ||
var paths = []; | ||
traverseRevTree(tree, function(isLeaf, pos, id, history) { | ||
Pouch.merge.traverseRevTree(tree, function(isLeaf, pos, id, history, opts) { | ||
history = history ? history.slice(0) : []; | ||
history.push(id); | ||
history.push({id: id, opts: opts}); | ||
if (isLeaf) { | ||
@@ -334,261 +256,30 @@ var rootPos = pos + 1 - history.length; | ||
var ajax = function ajax(options, callback) { | ||
if (typeof options === "function") { | ||
callback = options; | ||
options = {}; | ||
// check if a specific revision of a doc has been deleted | ||
// - metadata: the metadata object from the doc store | ||
// - rev: (optional) the revision to check. defaults to winning revision | ||
var isDeleted = function(metadata, rev) { | ||
if (!rev) { | ||
rev = Pouch.merge.winningRev(metadata); | ||
} | ||
var defaultOptions = { | ||
method : "GET", | ||
headers: {}, | ||
json: true, | ||
timeout: 10000 | ||
}; | ||
options = extend(true, defaultOptions, options); | ||
if (options.auth) { | ||
var token = btoa(options.auth.username + ':' + options.auth.password); | ||
options.headers.Authorization = 'Basic ' + token; | ||
if (rev.indexOf('-') >= 0) { | ||
rev = rev.split('-')[1]; | ||
} | ||
var onSuccess = function(obj, resp, cb){ | ||
if (!options.json && typeof obj !== 'string') { | ||
obj = JSON.stringify(obj); | ||
} else if (options.json && typeof obj === 'string') { | ||
obj = JSON.parse(obj); | ||
var deleted = false; | ||
Pouch.merge.traverseRevTree(metadata.rev_tree, function(isLeaf, pos, id, acc, opts) { | ||
if (id === rev) { | ||
deleted = !!opts.deleted; | ||
} | ||
call(cb, null, obj, resp); | ||
}; | ||
var onError = function(err, cb){ | ||
var errParsed; | ||
var errObj = err.responseText ? {status: err.status} : err; //this seems too clever | ||
try{ | ||
errParsed = JSON.parse(err.responseText); //would prefer not to have a try/catch clause | ||
errObj = extend(true, {}, errObj, errParsed); | ||
} catch(e){} | ||
call(cb, errObj); | ||
}; | ||
if (typeof window !== 'undefined' && window.XMLHttpRequest) { | ||
var timer,timedout = false; | ||
var xhr = new XMLHttpRequest(); | ||
xhr.open(options.method, options.url); | ||
if (options.json) { | ||
options.headers.Accept = 'application/json'; | ||
options.headers['Content-Type'] = 'application/json'; | ||
if (options.body && typeof options.body !== "string") { | ||
options.body = JSON.stringify(options.body); | ||
} | ||
} | ||
for (var key in options.headers){ | ||
xhr.setRequestHeader(key, options.headers[key]); | ||
} | ||
if (!("body" in options)) { | ||
options.body = null; | ||
} | ||
}); | ||
var abortReq = function() { | ||
timedout=true; | ||
xhr.abort(); | ||
call(onError, xhr, callback); | ||
}; | ||
xhr.onreadystatechange = function() { | ||
if (xhr.readyState !== 4 || timedout) { | ||
return; | ||
} | ||
clearTimeout(timer); | ||
if (xhr.status >= 200 && xhr.status < 300){ | ||
call(onSuccess, xhr.responseText, xhr, callback); | ||
} else { | ||
call(onError, xhr, callback); | ||
} | ||
}; | ||
if (options.timeout > 0) { | ||
timer = setTimeout(abortReq, options.timeout); | ||
} | ||
xhr.send(options.body); | ||
return {abort:abortReq}; | ||
} else { | ||
if (options.json) { | ||
options.headers.Accept = 'application/json'; | ||
options.headers['Content-Type'] = 'application/json'; | ||
} | ||
return request(options, function(err, response, body) { | ||
if (err) { | ||
err.status = response ? response.statusCode : 400; | ||
return call(onError, err, callback); | ||
} | ||
var content_type = response.headers['content-type']; | ||
var data = (body || ''); | ||
// CouchDB doesn't always return the right content-type for JSON data, so | ||
// we check for ^{ and }$ (ignoring leading/trailing whitespace) | ||
if (options.json && typeof data !== 'object' && | ||
(/json/.test(content_type) || | ||
(/^[\s]*\{/.test(data) && /\}[\s]*$/.test(data)))) { | ||
data = JSON.parse(data); | ||
} | ||
if (data.error) { | ||
data.status = response.statusCode; | ||
call(onError, data, callback); | ||
} | ||
else { | ||
call(onSuccess, data, response, callback); | ||
} | ||
}); | ||
} | ||
return deleted; | ||
}; | ||
// Extends method | ||
// (taken from http://code.jquery.com/jquery-1.9.0.js) | ||
// Populate the class2type map | ||
var class2type = {}; | ||
var types = ["Boolean", "Number", "String", "Function", "Array", "Date", "RegExp", "Object", "Error"]; | ||
for (var i = 0; i < types.length; i++) { | ||
var typename = types[i]; | ||
class2type[ "[object " + typename + "]" ] = typename.toLowerCase(); | ||
} | ||
var core_toString = class2type.toString; | ||
var core_hasOwn = class2type.hasOwnProperty; | ||
var type = function(obj) { | ||
if (obj === null) { | ||
return String( obj ); | ||
} | ||
return typeof obj === "object" || typeof obj === "function" ? | ||
class2type[core_toString.call(obj)] || "object" : | ||
typeof obj; | ||
var isChromeApp = function(){ | ||
return (typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.local !== "undefined"); | ||
}; | ||
var isWindow = function(obj) { | ||
return obj !== null && obj === obj.window; | ||
var isCordova = function(){ | ||
return (typeof cordova !== "undefined" || typeof PhoneGap !== "undefined" || typeof phonegap !== "undefined"); | ||
}; | ||
var isPlainObject = function( obj ) { | ||
// Must be an Object. | ||
// Because of IE, we also have to check the presence of the constructor property. | ||
// Make sure that DOM nodes and window objects don't pass through, as well | ||
if ( !obj || type(obj) !== "object" || obj.nodeType || isWindow( obj ) ) { | ||
return false; | ||
} | ||
try { | ||
// Not own constructor property must be Object | ||
if ( obj.constructor && | ||
!core_hasOwn.call(obj, "constructor") && | ||
!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { | ||
return false; | ||
} | ||
} catch ( e ) { | ||
// IE8,9 Will throw exceptions on certain host objects #9897 | ||
return false; | ||
} | ||
// Own properties are enumerated firstly, so to speed up, | ||
// if last one is own, then all properties are own. | ||
var key; | ||
for ( key in obj ) {} | ||
return key === undefined || core_hasOwn.call( obj, key ); | ||
}; | ||
var isFunction = function(obj) { | ||
return type(obj) === "function"; | ||
}; | ||
var isArray = Array.isArray || function(obj) { | ||
return type(obj) === "array"; | ||
}; | ||
var extend = function() { | ||
var options, name, src, copy, copyIsArray, clone, | ||
target = arguments[0] || {}, | ||
i = 1, | ||
length = arguments.length, | ||
deep = false; | ||
// Handle a deep copy situation | ||
if ( typeof target === "boolean" ) { | ||
deep = target; | ||
target = arguments[1] || {}; | ||
// skip the boolean and the target | ||
i = 2; | ||
} | ||
// Handle case when target is a string or something (possible in deep copy) | ||
if ( typeof target !== "object" && !isFunction(target) ) { | ||
target = {}; | ||
} | ||
// extend jQuery itself if only one argument is passed | ||
if ( length === i ) { | ||
target = this; | ||
--i; | ||
} | ||
for ( ; i < length; i++ ) { | ||
// Only deal with non-null/undefined values | ||
if ((options = arguments[ i ]) != null) { | ||
// Extend the base object | ||
for ( name in options ) { | ||
src = target[ name ]; | ||
copy = options[ name ]; | ||
// Prevent never-ending loop | ||
if ( target === copy ) { | ||
continue; | ||
} | ||
// Recurse if we're merging plain objects or arrays | ||
if ( deep && copy && ( isPlainObject(copy) || (copyIsArray = isArray(copy)) ) ) { | ||
if ( copyIsArray ) { | ||
copyIsArray = false; | ||
clone = src && isArray(src) ? src : []; | ||
} else { | ||
clone = src && isPlainObject(src) ? src : {}; | ||
} | ||
// Never move original objects, clone them | ||
target[ name ] = extend( deep, clone, copy ); | ||
// Don't bring in undefined values | ||
} else if ( copy !== undefined ) { | ||
target[ name ] = copy; | ||
} | ||
} | ||
} | ||
} | ||
// Return the modified object | ||
return target; | ||
}; | ||
// Basic wrapper for localStorage | ||
var win = this; | ||
var localJSON = (function(){ | ||
if (!win.localStorage) { | ||
return false; | ||
} | ||
return { | ||
set: function(prop, val) { | ||
localStorage.setItem(prop, JSON.stringify(val)); | ||
}, | ||
get: function(prop, def) { | ||
try { | ||
if (localStorage.getItem(prop) === null) { | ||
return def; | ||
} | ||
return JSON.parse((localStorage.getItem(prop) || 'false')); | ||
} catch(err) { | ||
return def; | ||
} | ||
}, | ||
remove: function(prop) { | ||
localStorage.removeItem(prop); | ||
} | ||
}; | ||
})(); | ||
if (typeof module !== 'undefined' && module.exports) { | ||
@@ -602,2 +293,5 @@ // use node.js's crypto library instead of the Crypto object created by deps/uuid.js | ||
}; | ||
var extend = require('./deps/extend'); | ||
var ajax = require('./deps/ajax'); | ||
request = require('request'); | ||
@@ -617,10 +311,5 @@ _ = require('underscore'); | ||
compareRevs: compareRevs, | ||
collectRevs: collectRevs, | ||
collectLeaves: collectLeaves, | ||
collectConflicts: collectConflicts, | ||
fetchCheckpoint: fetchCheckpoint, | ||
writeCheckpoint: writeCheckpoint, | ||
computeHeight: computeHeight, | ||
arrayFirst: arrayFirst, | ||
filterChange: filterChange, | ||
ajax: ajax, | ||
atob: function(str) { | ||
@@ -633,6 +322,6 @@ return decodeURIComponent(escape(new Buffer(str, 'base64').toString('binary'))); | ||
extend: extend, | ||
traverseRevTree: traverseRevTree, | ||
ajax: ajax, | ||
rootToLeaf: rootToLeaf, | ||
isPlainObject: isPlainObject, | ||
isArray: isArray | ||
isChromeApp: isChromeApp, | ||
isCordova: isCordova | ||
}; | ||
@@ -646,5 +335,12 @@ } | ||
window.addEventListener("storage", function(e) { | ||
api.notify(e.key); | ||
}); | ||
if (isChromeApp()){ | ||
chrome.storage.onChanged.addListener(function(e){ | ||
api.notify(e.db_name.newValue);//object only has oldValue, newValue members | ||
}); | ||
} | ||
else { | ||
window.addEventListener("storage", function(e) { | ||
api.notify(e.key); | ||
}); | ||
} | ||
@@ -669,6 +365,16 @@ api.addListener = function(db_name, id, db, opts) { | ||
api.notifyLocalWindows = function(db_name){ | ||
//do a useless change on a storage thing | ||
//in order to get other windows's listeners to activate | ||
if (!isChromeApp()){ | ||
localStorage[db_name] = (localStorage[db_name] === "a") ? "b" : "a"; | ||
} else { | ||
chrome.storage.local.set({db_name: db_name}); | ||
} | ||
}; | ||
api.notify = function(db_name) { | ||
if (!listeners[db_name]) { return; } | ||
for (var i in listeners[db_name]) { | ||
/*jshint loopfunc: true */ | ||
Object.keys(listeners[db_name]).forEach(function (i) { | ||
var opts = listeners[db_name][i].opts; | ||
@@ -679,6 +385,8 @@ listeners[db_name][i].db.changes({ | ||
continuous: false, | ||
descending: false, | ||
filter: opts.filter, | ||
since: opts.since, | ||
query_params: opts.query_params, | ||
onChange: function(c) { | ||
if (c.seq > opts.since) { | ||
if (c.seq > opts.since && !opts.cancelled) { | ||
opts.since = c.seq; | ||
@@ -689,3 +397,3 @@ call(opts.onChange, c); | ||
}); | ||
} | ||
}); | ||
}; | ||
@@ -692,0 +400,0 @@ |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
1
10
12
661357
17
43
12000