UNPKG

most-couchdb

Version:

most (data streaming) for CouchDB

587 lines (539 loc) 16.9 kB
// 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);