UNPKG

most-couchdb

Version:

most (data streaming) for CouchDB

460 lines (366 loc) 12.5 kB
This is a minimalist CouchDB (HTTP) API It provides exactly what this module needs, but no more. LRU = require 'lru-cache' http = require 'http' https = require 'https' lru_options = max: 200 dispose: (key,{source}) -> debug 'lru_cache: dispose', key 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 = (timeout) -> new Promise (resolve) -> setTimeout resolve, timeout class CouchDB constructor: (uri,options,limit = 100) -> options ?= {} options = {use_lru: options} if typeof options is 'boolean' # legacy Parse options @limit = options.limit ? limit {@poll_delay} = options if uri.match /\/$/ @uri = uri.slice 0, -1 else @uri = uri if options.use_lru @cache = lru_cache else @cache = static_cache switch when uri.match /^http:/ @agent = Request.agent http_agent when uri.match /^https:/ @agent = Request.agent https_agent else @agent = Request return info: -> @agent .get @uri .accept 'json' .then ({body}) -> body create: (n) -> query = {} query.n = n if n? @agent .put @uri .query query .accept 'json' .then ({body}) -> body destroy: -> @agent .delete @uri .accept 'json' .then ({body}) -> body Insert a document in the database (document must have valid `_id` and `_rev` fields). put: (doc) -> {_id} = doc uri = new URL ec(_id), @uri+'/' @agent .put uri.toString() .type 'json' .accept 'json' .send doc .then ({body}) -> body Get a document, optionally at a given revision. get: (_id,options = {}) -> uri = new URL ec(_id), @uri+'/' for own k,v of options when v? uri.searchParams.set k, v @agent .get uri.toString() .accept 'json' .then ({body}) -> body has: (_id,options = {}) -> uri = new URL ec(_id), @uri+'/' for own k,v of options when v? uri.searchParams.set k, v @agent .get uri.toString() .accept 'json' .then -> true .catch (err) -> if err.status is 404 false else Promise.reject err Delete a document based on its `_id` and `_rev` fields. delete: ({_id,_rev}) -> uri = new URL ec(_id), @uri+'/' uri.searchParams.set 'rev', _rev if _rev? @agent .delete uri.toString() .accept 'json' .then ({body}) -> body Basic support for Mango queries and indexes Non-blocking (most.js) find: (params) -> fromAsyncIterable @findAsyncIterable params Blocking (Stream) findStream: (params) -> streamify @findAsyncIterable params findAsyncIterable: (params,cancel) -> uri = new URL '_find', @uri+'/' agent = @agent our_limit = @limit poll_delay = @poll_delay do -> bookmark = null loop limit = our_limit body = null until body? return if cancel?() {body} = await agent .post uri.toString() .send Object.assign {bookmark,limit}, params .accept 'json' .catch (error) -> debug 'findAsyncIterable: error', params, error switch when error.status is 404 body: docs: [] when error.code is 'ETOOLARGE' limit-- if limit > 1 body: null else body: null unless body? await sleep 100 {docs} = body for doc from docs yield doc {bookmark} = body return if docs.length < limit await sleep poll_delay if poll_delay? return createIndex: (params) -> uri = new URL '_index', @uri+'/' @agent .post uri.toString() .send params .accept 'json' .then ({body}) -> body Uses a server-side view, returns a stream containing one event for each row. Non-blocking (most.js) query: (app,view,params) -> fromAsyncIterable @queryAsyncIterable app, view, params Blocking (Stream) queryStream: (app,view,params) -> streamify @queryAsyncIterable app, view, params Async Iterable queryAsyncIterable: (app,view,params,cancel) -> if app? uri = new URL "_design/#{app}/_view/#{view}", @uri+'/' else uri = new URL view, @uri+'/' agent = @agent our_limit = @limit poll_delay = @poll_delay query = Object.assign {}, params Normalize the request if query.startkey? query.start_key ?= query.startkey delete query.startkey if query.endkey? query.end_key ?= query.endkey delete query.endkey if query.key? query.keys = [query.key] delete query.key Build the ranges switch when query.keys? {keys} = query ranges = -> for key in keys yield start_key:key,end_key:key,inclusive_end:true return else {start_key,end_key,inclusive_end} = query ranges = -> yield {start_key,end_key,inclusive_end} return delete query.keys delete query.start_key delete query.end_key delete query.inclusive_end do -> for range from ranges() query.startkey = range.start_key query.endkey = range.end_key query.inclusive_end = range.inclusive_end loop limit = our_limit query.sorted = true body = null until body? return if cancel?() {body} = await agent .get uri.toString() .query stringify Object.assign {limit}, query .accept 'json' .catch (error) -> debug 'queryAsyncIterable: error', app, view, params, error switch when error.status is 404 body: rows: [] when error.code is 'ETOOLARGE' limit-- if limit > 1 body: null else body: null unless body? await sleep 100 {rows} = body if rows.length is limit next_row = rows.pop() else next_row = null for row in rows yield row if query.limit? query.limit-- return if query.limit is 0 if next_row? query.startkey = next_row.key query.startkey_docid = next_row.id await sleep poll_delay if poll_delay? else delete query.startkey_docid break return 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) -> fromAsyncIterable @query_changesAsyncIterable map_function, options query_changesStream: (map_function,options) -> streamify @query_changesAsyncIterable map_function, options query_changesAsyncIterable: (map_function,options) -> {since,filter,selector,view,include_docs} = options ? {} S = @changesAsyncIterable {live:true,include_docs:true,since,filter,selector,view} for await {id,seq,deleted,doc} from S out = [] emit = (key,value) -> content = {id,seq,deleted,key,value} content.doc = doc if include_docs out.push content return fn = map_function emit fn Object.assign {}, doc # might throw for item in out yield item return Build a continuous, non-blocking (`most.js`) stream for changes. changes: (options) -> fromAsyncIterable @changesAsyncIterable options Blocking (Stream) changesStream: (options) -> streamify @changesAsyncIterable options Async Iterable changesAsyncIterable: (options,cancel) -> uri = new URL '_changes', @uri+'/' agent = @agent our_limit = @limit poll_delay = @poll_delay query = {} content = {} query.feed = 'longpoll' query.heartbeat = 5*1000 query.timeout = 30*1000 options ?= {} query.include_docs = true if options.include_docs query.conflicts = true if options.conflicts query.attachments = true if options.attachments query.filter = options.filter if options.filter? switch when options.selector? query.filter = '_selector' content = selector: options.selector when options.view? query.filter = '_view' query.view = options.view when options.doc_ids? query.filter = '_doc_ids' content = doc_ids: options.doc_ids since = options.since ? 'now' do -> loop limit = our_limit body = null until body? return if cancel?() {body} = await agent .post uri.toString() .query stringify Object.assign {since,limit}, query .send content .accept 'json' .catch (error) -> debug 'changesAsyncIterable: error', options, error switch when error.status is 404 body: results:[], last_seq: null when error.code is 'ETOOLARGE' limit-- if limit > 1 body: null else body: null unless body? await sleep 100 {results} = body for result in results yield result {last_seq} = body return if not last_seq? since = last_seq await sleep poll_delay if poll_delay? return getAttachment: (_id,file) -> uri = new URL ec(_id)+'/'+encodeURI(file), @uri+'/' @agent .get uri.toString() .then ({body}) -> body putAttachment: (_id,file,rev,buf,type) -> uri = new URL ec(_id)+'/'+encodeURI(file), @uri+'/' @agent .put uri.toString() .query {rev} .type type .accept 'json' .send buf .then ({body}) -> body deleteAttachment: (_id,file,rev) -> uri = new URL ec(_id)+'/'+encodeURI(file), @uri+'/' @agent .delete uri.toString() .query {rev} .accept 'json' .then ({body}) -> 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 = (params) -> params = Object.assign {}, params ? {} ['endkey','end_key','key','keys','startkey','start_key'].forEach (field) -> if field of params params[field] = JSON.stringify params[field] return params