most-couchdb
Version:
most (data streaming) for CouchDB
587 lines (539 loc) • 16.9 kB
JavaScript
// Generated by CoffeeScript 2.4.1
(function() {
// This is a minimalist CouchDB (HTTP) API
// It provides exactly what this module needs, but no more.
var CouchDB, LRU, Request, URL, debug, ec, fromAsyncIterable, http, http_agent, https, https_agent, lru_cache, lru_options, sleep, static_cache, streamify, stringify,
hasProp = {}.hasOwnProperty;
LRU = require('lru-cache');
http = require('http');
https = require('https');
lru_options = {
max: 200,
dispose: function(key, {source}) {
debug('lru_cache: dispose', key);
return source.close();
},
maxAge: 20 * 60 * 1000
};
lru_cache = new LRU(lru_options);
lru_cache.delete = lru_cache.del;
static_cache = new Map();
http_agent = new http.Agent({
keepAlive: true,
// keepAliveMsecs: 100
maxSockets: 768, // per host
maxFreeSockets: 256, // per host
timeout: 30000 // active socket keepalive
});
https_agent = new https.Agent({
keepAlive: true,
// keepAliveMsecs: 100
maxSockets: 768, // per host
maxFreeSockets: 256, // per host
timeout: 30000 // active socket keepalive
});
sleep = function(timeout) {
return new Promise(function(resolve) {
return setTimeout(resolve, timeout);
});
};
CouchDB = class CouchDB {
constructor(uri, options, limit = 100) {
var ref;
if (options == null) {
options = {};
}
if (typeof options === 'boolean') { // legacy
options = {
use_lru: options
};
}
// Parse options
this.limit = (ref = options.limit) != null ? ref : limit;
({poll_delay: this.poll_delay} = options);
if (uri.match(/\/$/)) {
this.uri = uri.slice(0, -1);
} else {
this.uri = uri;
}
if (options.use_lru) {
this.cache = lru_cache;
} else {
this.cache = static_cache;
}
switch (false) {
case !uri.match(/^http:/):
this.agent = Request.agent(http_agent);
break;
case !uri.match(/^https:/):
this.agent = Request.agent(https_agent);
break;
default:
this.agent = Request;
}
return;
}
info() {
return this.agent.get(this.uri).accept('json').then(function({body}) {
return body;
});
}
create(n) {
var query;
query = {};
if (n != null) {
query.n = n;
}
return this.agent.put(this.uri).query(query).accept('json').then(function({body}) {
return body;
});
}
destroy() {
return this.agent.delete(this.uri).accept('json').then(function({body}) {
return body;
});
}
// Insert a document in the database (document must have valid `_id` and `_rev` fields).
put(doc) {
var _id, uri;
({_id} = doc);
uri = new URL(ec(_id), this.uri + '/');
return this.agent.put(uri.toString()).type('json').accept('json').send(doc).then(function({body}) {
return body;
});
}
// Get a document, optionally at a given revision.
get(_id, options = {}) {
var k, uri, v;
uri = new URL(ec(_id), this.uri + '/');
for (k in options) {
if (!hasProp.call(options, k)) continue;
v = options[k];
if (v != null) {
uri.searchParams.set(k, v);
}
}
return this.agent.get(uri.toString()).accept('json').then(function({body}) {
return body;
});
}
has(_id, options = {}) {
var k, uri, v;
uri = new URL(ec(_id), this.uri + '/');
for (k in options) {
if (!hasProp.call(options, k)) continue;
v = options[k];
if (v != null) {
uri.searchParams.set(k, v);
}
}
return this.agent.get(uri.toString()).accept('json').then(function() {
return true;
}).catch(function(err) {
if (err.status === 404) {
return false;
} else {
return Promise.reject(err);
}
});
}
// Delete a document based on its `_id` and `_rev` fields.
delete({_id, _rev}) {
var uri;
uri = new URL(ec(_id), this.uri + '/');
if (_rev != null) {
uri.searchParams.set('rev', _rev);
}
return this.agent.delete(uri.toString()).accept('json').then(function({body}) {
return body;
});
}
// Basic support for Mango queries and indexes
// Non-blocking (most.js)
find(params) {
return fromAsyncIterable(this.findAsyncIterable(params));
}
// Blocking (Stream)
findStream(params) {
return streamify(this.findAsyncIterable(params));
}
findAsyncIterable(params, cancel) {
var agent, our_limit, poll_delay, uri;
uri = new URL('_find', this.uri + '/');
agent = this.agent;
our_limit = this.limit;
poll_delay = this.poll_delay;
return (async function*() {
var body, bookmark, doc, docs, limit;
bookmark = null;
while (true) {
limit = our_limit;
body = null;
while (body == null) {
if (typeof cancel === "function" ? cancel() : void 0) {
return;
}
({body} = (await agent.post(uri.toString()).send(Object.assign({bookmark, limit}, params)).accept('json').catch(function(error) {
debug('findAsyncIterable: error', params, error);
switch (false) {
case error.status !== 404:
return {
body: {
docs: []
}
};
case error.code !== 'ETOOLARGE':
if (limit > 1) {
limit--;
}
return {
body: null
};
default:
return {
body: null
};
}
})));
if (body == null) {
await sleep(100);
}
}
({docs} = body);
for (doc of docs) {
yield doc;
}
({bookmark} = body);
if (docs.length < limit) {
return;
}
if (poll_delay != null) {
await sleep(poll_delay);
}
}
})();
}
createIndex(params) {
var uri;
uri = new URL('_index', this.uri + '/');
return this.agent.post(uri.toString()).send(params).accept('json').then(function({body}) {
return body;
});
}
// Uses a server-side view, returns a stream containing one event for each row.
// Non-blocking (most.js)
query(app, view, params) {
return fromAsyncIterable(this.queryAsyncIterable(app, view, params));
}
// Blocking (Stream)
queryStream(app, view, params) {
return streamify(this.queryAsyncIterable(app, view, params));
}
// Async Iterable
queryAsyncIterable(app, view, params, cancel) {
var agent, end_key, inclusive_end, keys, our_limit, poll_delay, query, ranges, start_key, uri;
if (app != null) {
uri = new URL(`_design/${app}/_view/${view}`, this.uri + '/');
} else {
uri = new URL(view, this.uri + '/');
}
agent = this.agent;
our_limit = this.limit;
poll_delay = this.poll_delay;
query = Object.assign({}, params);
// Normalize the request
if (query.startkey != null) {
if (query.start_key == null) {
query.start_key = query.startkey;
}
delete query.startkey;
}
if (query.endkey != null) {
if (query.end_key == null) {
query.end_key = query.endkey;
}
delete query.endkey;
}
if (query.key != null) {
query.keys = [query.key];
delete query.key;
}
switch (false) {
// Build the ranges
case query.keys == null:
({keys} = query);
ranges = function*() {
var i, key, len;
for (i = 0, len = keys.length; i < len; i++) {
key = keys[i];
yield ({
start_key: key,
end_key: key,
inclusive_end: true
});
}
};
break;
default:
({start_key, end_key, inclusive_end} = query);
ranges = function*() {
yield ({start_key, end_key, inclusive_end});
};
}
delete query.keys;
delete query.start_key;
delete query.end_key;
delete query.inclusive_end;
return (async function*() {
var body, i, len, limit, next_row, range, ref, row, rows;
ref = ranges();
for (range of ref) {
query.startkey = range.start_key;
query.endkey = range.end_key;
query.inclusive_end = range.inclusive_end;
while (true) {
limit = our_limit;
query.sorted = true;
body = null;
while (body == null) {
if (typeof cancel === "function" ? cancel() : void 0) {
return;
}
({body} = (await agent.get(uri.toString()).query(stringify(Object.assign({limit}, query))).accept('json').catch(function(error) {
debug('queryAsyncIterable: error', app, view, params, error);
switch (false) {
case error.status !== 404:
return {
body: {
rows: []
}
};
case error.code !== 'ETOOLARGE':
if (limit > 1) {
limit--;
}
return {
body: null
};
default:
return {
body: null
};
}
})));
if (body == null) {
await sleep(100);
}
}
({rows} = body);
if (rows.length === limit) {
next_row = rows.pop();
} else {
next_row = null;
}
for (i = 0, len = rows.length; i < len; i++) {
row = rows[i];
yield row;
if (query.limit != null) {
query.limit--;
if (query.limit === 0) {
return;
}
}
}
if (next_row != null) {
query.startkey = next_row.key;
query.startkey_docid = next_row.id;
if (poll_delay != null) {
await sleep(poll_delay);
}
} else {
delete query.startkey_docid;
break;
}
}
}
})();
}
// Uses a wrapped client-side map function, returns a stream containing one event for each new row.
// Please provide `map_function(emit)`, wrapping the actual `map` function.
query_changes(map_function, options) {
return fromAsyncIterable(this.query_changesAsyncIterable(map_function, options));
}
query_changesStream(map_function, options) {
return streamify(this.query_changesAsyncIterable(map_function, options));
}
async * query_changesAsyncIterable(map_function, options) {
var S, deleted, doc, emit, filter, fn, i, id, include_docs, item, len, out, selector, seq, since, view, x;
({since, filter, selector, view, include_docs} = options != null ? options : {});
S = this.changesAsyncIterable({
live: true,
include_docs: true,
since,
filter,
selector,
view
});
for await (x of S) {
({id, seq, deleted, doc} = x);
out = [];
emit = function(key, value) {
var content;
content = {id, seq, deleted, key, value};
if (include_docs) {
content.doc = doc;
}
out.push(content);
};
fn = map_function(emit);
fn(Object.assign({}, doc)); // might throw
for (i = 0, len = out.length; i < len; i++) {
item = out[i];
yield item;
}
}
}
// Build a continuous, non-blocking (`most.js`) stream for changes.
changes(options) {
return fromAsyncIterable(this.changesAsyncIterable(options));
}
// Blocking (Stream)
changesStream(options) {
return streamify(this.changesAsyncIterable(options));
}
// Async Iterable
changesAsyncIterable(options, cancel) {
var agent, content, our_limit, poll_delay, query, ref, since, uri;
uri = new URL('_changes', this.uri + '/');
agent = this.agent;
our_limit = this.limit;
poll_delay = this.poll_delay;
query = {};
content = {};
query.feed = 'longpoll';
query.heartbeat = 5 * 1000;
query.timeout = 30 * 1000;
if (options == null) {
options = {};
}
if (options.include_docs) {
query.include_docs = true;
}
if (options.conflicts) {
query.conflicts = true;
}
if (options.attachments) {
query.attachments = true;
}
if (options.filter != null) {
query.filter = options.filter;
}
switch (false) {
case options.selector == null:
query.filter = '_selector';
content = {
selector: options.selector
};
break;
case options.view == null:
query.filter = '_view';
query.view = options.view;
break;
case options.doc_ids == null:
query.filter = '_doc_ids';
content = {
doc_ids: options.doc_ids
};
}
since = (ref = options.since) != null ? ref : 'now';
return (async function*() {
var body, i, last_seq, len, limit, result, results;
while (true) {
limit = our_limit;
body = null;
while (body == null) {
if (typeof cancel === "function" ? cancel() : void 0) {
return;
}
({body} = (await agent.post(uri.toString()).query(stringify(Object.assign({since, limit}, query))).send(content).accept('json').catch(function(error) {
debug('changesAsyncIterable: error', options, error);
switch (false) {
case error.status !== 404:
return {
body: {
results: [],
last_seq: null
}
};
case error.code !== 'ETOOLARGE':
if (limit > 1) {
limit--;
}
return {
body: null
};
default:
return {
body: null
};
}
})));
if (body == null) {
await sleep(100);
}
}
({results} = body);
for (i = 0, len = results.length; i < len; i++) {
result = results[i];
yield result;
}
({last_seq} = body);
if (last_seq == null) {
return;
}
since = last_seq;
if (poll_delay != null) {
await sleep(poll_delay);
}
}
})();
}
getAttachment(_id, file) {
var uri;
uri = new URL(ec(_id) + '/' + encodeURI(file), this.uri + '/');
return this.agent.get(uri.toString()).then(function({body}) {
return body;
});
}
putAttachment(_id, file, rev, buf, type) {
var uri;
uri = new URL(ec(_id) + '/' + encodeURI(file), this.uri + '/');
return this.agent.put(uri.toString()).query({rev}).type(type).accept('json').send(buf).then(function({body}) {
return body;
});
}
deleteAttachment(_id, file, rev) {
var uri;
uri = new URL(ec(_id) + '/' + encodeURI(file), this.uri + '/');
return this.agent.delete(uri.toString()).query({rev}).accept('json').then(function({body}) {
return body;
});
}
};
module.exports = CouchDB;
ec = encodeURIComponent;
({URL} = require('url'));
Request = require('superagent');
debug = (require('debug'))('most-couchdb');
streamify = require('async-stream-generator');
({fromAsyncIterable} = require('most-async-iterable'));
stringify = function(params) {
params = Object.assign({}, params != null ? params : {});
['endkey', 'end_key', 'key', 'keys', 'startkey', 'start_key'].forEach(function(field) {
if (field in params) {
params[field] = JSON.stringify(params[field]);
}
});
return params;
};
}).call(this);