nqm-minimongo
Version:
Client-side mongo database with server sync over http
263 lines (223 loc) • 7.95 kB
text/coffeescript
_ = require 'lodash'
async = require 'async'
IDBStore = require 'idb-wrapper'
createUid = require('./utils').createUid
processFind = require('./utils').processFind
compileSort = require('./selector').compileSort
# Create a database backed by IndexedDb. options must contain namespace: <string to uniquely identify database>
module.exports = class IndexedDb
constructor: (options, success, error) ->
@collections = {}
# Create database
@store = new IDBStore {
dbVersion: 1
storeName: 'minimongo_' + options.namespace
keyPath: ['col', 'doc._id']
autoIncrement: false
onStoreReady: => if success then success(this)
onError: error
indexes: [
{ name: 'col', keyPath: 'col', unique: false, multiEntry: false }
{ name: 'col-state', keyPath: ['col', 'state'], unique: false, multiEntry: false}
]
}
addCollection: (name, success, error) ->
collection = new Collection(name, @store)
@[name] = collection
@collections[name] = collection
if success
success()
removeCollection: (name, success, error) ->
delete @[name]
delete @collections[name]
# Remove all documents
@store.query (matches) =>
keys = _.map matches, (m) -> [ m.col, m.doc._id ]
if keys.length > 0
@store.removeBatch keys, ->
if success? then success()
, error
else
if success? then success()
, { index: "col", keyRange: @store.makeKeyRange(only: name), onError: error }
# Stores data in indexeddb store
class Collection
constructor: (name, store) ->
@name = name
@store = store
find: (selector, options) ->
return fetch: (success, error) =>
@_findFetch(selector, options, success, error)
findOne: (selector, options, success, error) ->
if _.isFunction(options)
[options, success, error] = [{}, options, success]
@find(selector, options).fetch (results) ->
if success? then success(if results.length>0 then results[0] else null)
, error
_findFetch: (selector, options, success, error) ->
# Get all docs from collection
@store.query (matches) ->
# Filter removed docs
matches = _.filter matches, (m) -> m.state != "removed"
if success? then success(processFind(_.pluck(matches, "doc"), selector, options))
, { index: "col", keyRange: @store.makeKeyRange(only: @name), onError: error }
upsert: (doc, success, error) ->
# Handle both single and multiple upsert
items = doc
if not _.isArray(items)
items = [items]
for item in items
if not item._id
item._id = createUid()
records = _.map items, (item) =>
return {
col: @name
state: "upserted"
doc: item
}
@store.putBatch records, ->
if success then success(doc)
, error
remove: (id, success, error) ->
# Find record
@store.get [@name, id], (record) =>
# If not found, create placeholder record
if not record?
record = {
col: @name
doc: { _id: id }
}
# Set removed
record.state = "removed"
# Update
@store.put record, ->
if success then success(id)
, error
cache: (docs, selector, options, success, error) ->
step2 = =>
# Rows have been cached, now look for stale ones to remove
docsMap = _.object(_.pluck(docs, "_id"), docs)
if options.sort
sort = compileSort(options.sort)
# Perform query, removing rows missing in docs from local db
@find(selector, options).fetch (results) =>
removes = []
keys = _.map results, (result) => [@name, result._id]
if keys.length == 0
if success? then success()
return
@store.getBatch keys, (records) =>
for i in [0...records.length]
record = records[i]
result = results[i]
# If not present in docs and is present locally and not upserted/deleted
if not docsMap[result._id] and record and record.state == "cached"
# If past end on sorted limited, ignore
if options.sort and options.limit and docs.length == options.limit
if sort(result, _.last(docs)) >= 0
continue
# Item is gone from server, remove locally
removes.push [@name, result._id]
# If removes, handle them
if removes.length > 0
@store.removeBatch removes, ->
if success? then success()
, error
else
if success? then success()
, error
, error
if docs.length == 0
return step2()
# Create keys to get items
keys = _.map docs, (doc) => [@name, doc._id]
# Create batch of puts
puts = []
@store.getBatch keys, (records) =>
# Add all non-local that are not upserted or removed
for i in [0...records.length]
record = records[i]
doc = docs[i]
# Check if not present or not upserted/deleted
if not record? or record.state == "cached"
# If _rev present, make sure that not overwritten by lower _rev
if not record or not doc._rev or not record.doc._rev or doc._rev >= record.doc._rev
puts.push { col: @name, state: "cached", doc: doc }
# Put batch
if puts.length > 0
@store.putBatch puts, step2, error
else
step2()
, error
pendingUpserts: (success, error) ->
@store.query (matches) ->
if success? then success(_.pluck(matches, "doc"))
, { index: "col-state", keyRange: @store.makeKeyRange(only: [@name, "upserted"]), onError: error }
pendingRemoves: (success, error) ->
@store.query (matches) ->
if success? then success(_.pluck(_.pluck(matches, "doc"), "_id"))
, { index: "col-state", keyRange: @store.makeKeyRange(only: [@name, "removed"]), onError: error }
resolveUpsert: (doc, success, error) ->
# Handle both single and multiple upsert
items = doc
if not _.isArray(items)
items = [items]
# Get items
keys = _.map items, (item) => [@name, item._id]
@store.getBatch keys, (records) =>
puts = []
for i in [0...items.length]
record = records[i]
# Only safely remove upsert if doc is the same
if record and record.state == "upserted" and _.isEqual(record.doc, items[i])
record.state = "cached"
puts.push(record)
# Put all changed items
if puts.length > 0
@store.putBatch puts, ->
if success then success(doc)
, error
else
if success then success(doc)
, error
resolveRemove: (id, success, error) ->
@store.get [@name, id], (record) =>
# Only remove if removed
if record.state == "removed"
@store.remove [@name, id], ->
if success? then success()
, error
# Add but do not overwrite or record as upsert
seed: (doc, success, error) ->
@store.get [@name, doc._id], (record) =>
if not record?
record = {
col: @name
state: "cached"
doc: doc
}
@store.put record, ->
if success? then success()
, error
else
if success? then success()
# Add but do not overwrite upsert/removed and do not record as upsert
cacheOne: (doc, success, error) ->
@store.get [@name, doc._id], (record) =>
# If _rev present, make sure that not overwritten by lower _rev
if record and doc._rev and record.doc._rev and doc._rev < record.doc._rev
if success? then success()
return
if not record?
record = {
col: @name
state: "cached"
doc: doc
}
if record.state == "cached"
record.doc = doc
@store.put record, ->
if success? then success()
, error
else
if success? then success()