blob: ae929c7608422c90778e30696d45196595860ea4 [file] [log] [blame]
/*jshint strict: false */
/*global chrome */
var merge = require('./merge');
exports.extend = require('pouchdb-extend');
exports.ajax = require('./deps/ajax');
exports.createBlob = require('./deps/binary/blob');
exports.uuid = require('./deps/uuid');
exports.getArguments = require('argsarray');
var EventEmitter = require('events').EventEmitter;
var collections = require('pouchdb-collections');
exports.Map = collections.Map;
exports.Set = collections.Set;
var parseDoc = require('./deps/docs/parseDoc');
var Promise = require('./deps/promise');
exports.Promise = Promise;
var base64 = require('./deps/binary/base64');
// TODO: don't export these
exports.atob = base64.atob;
exports.btoa = base64.btoa;
var binStringToBlobOrBuffer =
require('./deps/binary/binaryStringToBlobOrBuffer');
var b64StringToBlobOrBuffer =
require('./deps/binary/base64StringToBlobOrBuffer');
// TODO: only used by the integration tests
exports.binaryStringToBlobOrBuffer = binStringToBlobOrBuffer;
// TODO: only used by mapreduce
exports.base64StringToBlobOrBuffer = b64StringToBlobOrBuffer;
exports.lastIndexOf = function (str, char) {
for (var i = str.length - 1; i >= 0; i--) {
if (str.charAt(i) === char) {
return i;
}
}
return -1;
};
exports.clone = function (obj) {
return exports.extend(true, {}, obj);
};
// like underscore/lodash _.pick()
function pick(obj, arr) {
var res = {};
for (var i = 0, len = arr.length; i < len; i++) {
var prop = arr[i];
if (prop in obj) {
res[prop] = obj[prop];
}
}
return res;
}
exports.pick = pick;
exports.inherits = require('inherits');
function isChromeApp() {
return (typeof chrome !== "undefined" &&
typeof chrome.storage !== "undefined" &&
typeof chrome.storage.local !== "undefined");
}
// Pretty dumb name for a function, just wraps callback calls so we dont
// to if (callback) callback() everywhere
exports.call = exports.getArguments(function (args) {
if (!args.length) {
return;
}
var fun = args.shift();
if (typeof fun === 'function') {
fun.apply(this, args);
}
});
exports.filterChange = function filterChange(opts) {
var req = {};
var hasFilter = opts.filter && typeof opts.filter === 'function';
req.query = opts.query_params;
return function filter(change) {
if (!change.doc) {
// CSG sends events on the changes feed that don't have documents,
// this hack makes a whole lot of existing code robust.
change.doc = {};
}
if (opts.filter && hasFilter && !opts.filter.call(this, change.doc, req)) {
return false;
}
if (!opts.include_docs) {
delete change.doc;
} else if (!opts.attachments) {
for (var att in change.doc._attachments) {
if (change.doc._attachments.hasOwnProperty(att)) {
change.doc._attachments[att].stub = true;
}
}
}
return true;
};
};
exports.parseDoc = parseDoc.parseDoc;
exports.invalidIdError = parseDoc.invalidIdError;
exports.isCordova = function () {
return (typeof cordova !== "undefined" ||
typeof PhoneGap !== "undefined" ||
typeof phonegap !== "undefined");
};
exports.hasLocalStorage = function () {
if (isChromeApp()) {
return false;
}
try {
return localStorage;
} catch (e) {
return false;
}
};
exports.Changes = Changes;
exports.inherits(Changes, EventEmitter);
function Changes() {
if (!(this instanceof Changes)) {
return new Changes();
}
var self = this;
EventEmitter.call(this);
this.isChrome = isChromeApp();
this._listeners = {};
this.hasLocal = false;
if (!this.isChrome) {
this.hasLocal = exports.hasLocalStorage();
}
if (this.isChrome) {
chrome.storage.onChanged.addListener(function (e) {
// make sure it's event addressed to us
if (e.db_name != null) {
//object only has oldValue, newValue members
self.emit(e.dbName.newValue);
}
});
} else if (this.hasLocal) {
if (typeof addEventListener !== 'undefined') {
addEventListener("storage", function (e) {
self.emit(e.key);
});
} else { // old IE
window.attachEvent("storage", function (e) {
self.emit(e.key);
});
}
}
}
Changes.prototype.addListener = function (dbName, id, db, opts) {
if (this._listeners[id]) {
return;
}
var self = this;
var inprogress = false;
function eventFunction() {
if (!self._listeners[id]) {
return;
}
if (inprogress) {
inprogress = 'waiting';
return;
}
inprogress = true;
var changesOpts = pick(opts, [
'style', 'include_docs', 'attachments', 'conflicts', 'filter',
'doc_ids', 'view', 'since', 'query_params', 'binary'
]);
db.changes(changesOpts).on('change', function (c) {
if (c.seq > opts.since && !opts.cancelled) {
opts.since = c.seq;
exports.call(opts.onChange, c);
}
}).on('complete', function () {
if (inprogress === 'waiting') {
process.nextTick(function () {
self.notify(dbName);
});
}
inprogress = false;
}).on('error', function () {
inprogress = false;
});
}
this._listeners[id] = eventFunction;
this.on(dbName, eventFunction);
};
Changes.prototype.removeListener = function (dbName, id) {
if (!(id in this._listeners)) {
return;
}
EventEmitter.prototype.removeListener.call(this, dbName,
this._listeners[id]);
};
Changes.prototype.notifyLocalWindows = function (dbName) {
//do a useless change on a storage thing
//in order to get other windows's listeners to activate
if (this.isChrome) {
chrome.storage.local.set({dbName: dbName});
} else if (this.hasLocal) {
localStorage[dbName] = (localStorage[dbName] === "a") ? "b" : "a";
}
};
Changes.prototype.notify = function (dbName) {
this.emit(dbName);
this.notifyLocalWindows(dbName);
};
exports.once = require('./deps/once');
exports.toPromise = require('./deps/toPromise');
exports.adapterFun = function (name, callback) {
var log = require('debug')('pouchdb:api');
function logApiCall(self, name, args) {
if (!log.enabled) {
return;
}
var logArgs = [self._db_name, name];
for (var i = 0; i < args.length - 1; i++) {
logArgs.push(args[i]);
}
log.apply(null, logArgs);
// override the callback itself to log the response
var origCallback = args[args.length - 1];
args[args.length - 1] = function (err, res) {
var responseArgs = [self._db_name, name];
responseArgs = responseArgs.concat(
err ? ['error', err] : ['success', res]
);
log.apply(null, responseArgs);
origCallback(err, res);
};
}
return exports.toPromise(exports.getArguments(function (args) {
if (this._closed) {
return Promise.reject(new Error('database is closed'));
}
var self = this;
logApiCall(self, name, args);
if (!this.taskqueue.isReady) {
return new Promise(function (fulfill, reject) {
self.taskqueue.addTask(function (failed) {
if (failed) {
reject(failed);
} else {
fulfill(self[name].apply(self, args));
}
});
});
}
return callback.apply(this, args);
}));
};
exports.cancellableFun = function (fun, self, opts) {
opts = opts ? exports.clone(true, {}, opts) : {};
var emitter = new EventEmitter();
var oldComplete = opts.complete || function () { };
var complete = opts.complete = exports.once(function (err, resp) {
if (err) {
oldComplete(err);
} else {
emitter.emit('end', resp);
oldComplete(null, resp);
}
emitter.removeAllListeners();
});
var oldOnChange = opts.onChange || function () {};
var lastChange = 0;
self.on('destroyed', function () {
emitter.removeAllListeners();
});
opts.onChange = function (change) {
oldOnChange(change);
if (change.seq <= lastChange) {
return;
}
lastChange = change.seq;
emitter.emit('change', change);
if (change.deleted) {
emitter.emit('delete', change);
} else if (change.changes.length === 1 &&
change.changes[0].rev.slice(0, 1) === '1-') {
emitter.emit('create', change);
} else {
emitter.emit('update', change);
}
};
var promise = new Promise(function (fulfill, reject) {
opts.complete = function (err, res) {
if (err) {
reject(err);
} else {
fulfill(res);
}
};
});
promise.then(function (result) {
complete(null, result);
}, complete);
// this needs to be overwridden by caller, dont fire complete until
// the task is ready
promise.cancel = function () {
promise.isCancelled = true;
if (self.taskqueue.isReady) {
opts.complete(null, {status: 'cancelled'});
}
};
if (!self.taskqueue.isReady) {
self.taskqueue.addTask(function () {
if (promise.isCancelled) {
opts.complete(null, {status: 'cancelled'});
} else {
fun(self, opts, promise);
}
});
} else {
fun(self, opts, promise);
}
promise.on = emitter.on.bind(emitter);
promise.once = emitter.once.bind(emitter);
promise.addListener = emitter.addListener.bind(emitter);
promise.removeListener = emitter.removeListener.bind(emitter);
promise.removeAllListeners = emitter.removeAllListeners.bind(emitter);
promise.setMaxListeners = emitter.setMaxListeners.bind(emitter);
promise.listeners = emitter.listeners.bind(emitter);
promise.emit = emitter.emit.bind(emitter);
return promise;
};
exports.explain404 = require('./deps/explain404');
exports.info = function (str) {
if (typeof console !== 'undefined' && 'info' in console) {
console.info(str);
}
};
exports.parseUri = require('./deps/parseUri');
exports.compare = function (left, right) {
return left < right ? -1 : left > right ? 1 : 0;
};
// compact a tree by marking its non-leafs as missing,
// and return a list of revs to delete
exports.compactTree = function compactTree(metadata) {
var revs = [];
merge.traverseRevTree(metadata.rev_tree, function (isLeaf, pos,
revHash, ctx, opts) {
if (opts.status === 'available' && !isLeaf) {
revs.push(pos + '-' + revHash);
opts.status = 'missing';
}
});
return revs;
};
var vuvuzela = require('vuvuzela');
exports.safeJsonParse = function safeJsonParse(str) {
try {
return JSON.parse(str);
} catch (e) {
return vuvuzela.parse(str);
}
};
exports.safeJsonStringify = function safeJsonStringify(json) {
try {
return JSON.stringify(json);
} catch (e) {
return vuvuzela.stringify(json);
}
};