most-couchdb
Version:
most (data streaming) for CouchDB
460 lines (366 loc) • 12.5 kB
Markdown
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
= options.limit ? limit
{} = options
if uri.match /\/$/
= uri.slice 0, -1
else
= uri
if options.use_lru
= lru_cache
else
= static_cache
switch
when uri.match /^http:/
= Request.agent http_agent
when uri.match /^https:/
= Request.agent https_agent
else
= Request
return
info: ->
.get
.accept 'json'
.then ({body}) -> body
create: (n) ->
query = {}
query.n = n if n?
.put
.query query
.accept 'json'
.then ({body}) -> body
destroy: ->
.delete
.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), +'/'
.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), +'/'
for own k,v of options when v?
uri.searchParams.set k, v
.get uri.toString()
.accept 'json'
.then ({body}) -> body
has: (_id,options = {}) ->
uri = new URL ec(_id), +'/'
for own k,v of options when v?
uri.searchParams.set k, v
.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.searchParams.set 'rev', _rev if _rev?
.delete uri.toString()
.accept 'json'
.then ({body}) -> body
Basic support for Mango queries and indexes
Non-blocking (most.js)
find: (params) ->
fromAsyncIterable params
Blocking (Stream)
findStream: (params) ->
streamify params
findAsyncIterable: (params,cancel) ->
uri = new URL '_find', +'/'
agent =
our_limit =
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', +'/'
.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 app, view, params
Blocking (Stream)
queryStream: (app,view,params) ->
streamify app, view, params
Async Iterable
queryAsyncIterable: (app,view,params,cancel) ->
if app?
uri = new URL "_design/#{app}/_view/#{view}", +'/'
else
uri = new URL view, +'/'
agent =
our_limit =
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 map_function, options
query_changesStream: (map_function,options) ->
streamify map_function, options
query_changesAsyncIterable: (map_function,options) ->
{since,filter,selector,view,include_docs} = options ? {}
S = {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 options
Blocking (Stream)
changesStream: (options) ->
streamify options
Async Iterable
changesAsyncIterable: (options,cancel) ->
uri = new URL '_changes', +'/'
agent =
our_limit =
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), +'/'
.get uri.toString()
.then ({body}) -> body
putAttachment: (_id,file,rev,buf,type) ->
uri = new URL ec(_id)+'/'+encodeURI(file), +'/'
.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), +'/'
.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