minimongo-sync
Version:
Client-side mongo database with server sync over http
267 lines (208 loc) • 8.26 kB
text/coffeescript
_ = require 'lodash'
async = require 'async'
utils = require('./utils')
processFind = require('./utils').processFind
compileSort = require('./selector').compileSort
module.exports = class LocalStorageDb
constructor: (options, success) ->
@collections = {}
if options and options.namespace and window.localStorage
@namespace = options.namespace
if success then success(this)
addCollection: (name, success, error) ->
# Set namespace for collection
namespace = @namespace+"."+name if @namespace
collection = new Collection(name, namespace)
@[name] = collection
@collections[name] = collection
if success? then success()
removeCollection: (name, success, error) ->
if @namespace and window.localStorage
keys = []
for i in [0...window.localStorage.length]
keys.push(window.localStorage.key(i))
for key in keys
keyToMatch = @namespace + '.' + name
if key.substring(0, keyToMatch.length) == keyToMatch
window.localStorage.removeItem(key)
delete @[name]
delete @collections[name]
if success? then success()
getCollectionNames: -> _.keys(@collections)
# Stores data in memory, optionally backed by local storage
class Collection
constructor: (name, namespace) ->
@name = name
@namespace = namespace
@items = {}
@upserts = {} # Pending upserts by _id. Still in items
@removes = {} # Pending removes by _id. No longer in items
# Read from local storage
if window.localStorage and namespace?
@loadStorage()
loadStorage: ->
# Read items from localStorage
@itemNamespace = @namespace + "_"
for i in [0...window.localStorage.length]
key = window.localStorage.key(i)
if key.substring(0, @itemNamespace.length) == @itemNamespace
item = JSON.parse(window.localStorage[key])
@items[item._id] = item
# Read upserts
upsertKeys = if window.localStorage[@namespace+"upserts"] then JSON.parse(window.localStorage[@namespace+"upserts"]) else []
for key in upsertKeys
@upserts[key] = { doc: @items[key] }
# Get base if present
base = if window.localStorage[@namespace+"upsertbase_"+key] then JSON.parse(window.localStorage[@namespace+"upsertbase_"+key]) else null
@upserts[key].base = base
# Read removes
removeItems = if window.localStorage[@namespace+"removes"] then JSON.parse(window.localStorage[@namespace+"removes"]) else []
@removes = _.object(_.pluck(removeItems, "_id"), removeItems)
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) ->
# Deep clone to prevent modification
if success? then success(processFind(_.cloneDeep(_.values(@items)), selector, options))
upsert: (docs, bases, success, error) ->
[items, success, error] = utils.regularizeUpsert(docs, bases, success, error)
# Keep independent copies to prevent modification
items = JSON.parse(JSON.stringify(items))
for item in items
# Fill in base
if item.base == undefined
# Use existing base
if @upserts[item.doc._id]
item.base = @upserts[item.doc._id].base
else
item.base = @items[item.doc._id] or null
# Keep independent copies
item = _.cloneDeep(item)
# Replace/add
@_putItem(item.doc)
@_putUpsert(item)
if success then success(docs)
remove: (id, success, error) ->
# Special case for filter-type remove
if _.isObject(id)
@find(id).fetch (rows) =>
async.each rows, (row, cb) =>
@remove(row._id, (=> cb()), cb)
, => success()
, error
return
if _.has(@items, id)
@_putRemove(@items[id])
@_deleteItem(id)
@_deleteUpsert(id)
else
@_putRemove({ _id: id })
if success? then success()
_putItem: (doc) ->
@items[doc._id] = doc
if @namespace
window.localStorage[@itemNamespace + doc._id] = JSON.stringify(doc)
_deleteItem: (id) ->
delete @items[id]
if @namespace
window.localStorage.removeItem(@itemNamespace + id)
_putUpsert: (upsert) ->
@upserts[upsert.doc._id] = upsert
if @namespace
window.localStorage[@namespace+"upserts"] = JSON.stringify(_.keys(@upserts))
window.localStorage[@namespace+"upsertbase_"+upsert.doc._id] = JSON.stringify(upsert.base)
_deleteUpsert: (id) ->
delete @upserts[id]
if @namespace
window.localStorage[@namespace+"upserts"] = JSON.stringify(_.keys(@upserts))
_putRemove: (doc) ->
@removes[doc._id] = doc
if @namespace
window.localStorage[@namespace+"removes"] = JSON.stringify(_.values(@removes))
_deleteRemove: (id) ->
delete @removes[id]
if @namespace
window.localStorage[@namespace+"removes"] = JSON.stringify(_.values(@removes))
cache: (docs, selector, options, success, error) ->
# Add all non-local that are not upserted or removed
for doc in docs
# Exclude any excluded _ids from being cached/uncached
if options and options.exclude and doc._id in options.exclude
continue
@cacheOne(doc)
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) =>
for result in results
if not docsMap[result._id] and not _.has(@upserts, result._id)
# Exclude any excluded _ids from being cached/uncached
if options and options.exclude and result._id in options.exclude
continue
# If at limit
if options.limit and docs.length == options.limit
# If past end on sorted limited, ignore
if options.sort and sort(result, _.last(docs)) >= 0
continue
# If no sort, ignore
if not options.sort
continue
@_deleteItem(result._id)
if success? then success()
, error
pendingUpserts: (success) ->
success _.values(@upserts)
pendingRemoves: (success) ->
success _.pluck(@removes, "_id")
resolveUpserts: (upserts, success) ->
for upsert in upserts
if @upserts[upsert.doc._id]
# Only safely remove upsert if item is unchanged
if _.isEqual(upsert.doc, @upserts[upsert.doc._id].doc)
@_deleteUpsert(upsert.doc._id)
else
# Just update base
@upserts[upsert.doc._id].base = upsert.doc
@_putUpsert(@upserts[upsert.doc._id])
if success? then success()
resolveRemove: (id, success) ->
@_deleteRemove(id)
if success? then success()
# Add but do not overwrite or record as upsert
seed: (docs, success) ->
if not _.isArray(docs)
docs = [docs]
for doc in docs
if not _.has(@items, doc._id) and not _.has(@removes, doc._id)
@_putItem(doc)
if success? then success()
# Add but do not overwrite upserts or removes
cacheOne: (doc, success, error) ->
@cacheList([doc], success, error)
# Add but do not overwrite upserts or removes
cacheList: (docs, success) ->
for doc in docs
if not _.has(@upserts, doc._id) and not _.has(@removes, doc._id)
existing = @items[doc._id]
# If _rev present, make sure that not overwritten by lower or equal _rev
if not existing or not doc._rev or not existing._rev or doc._rev > existing._rev
@_putItem(doc)
if success? then success()
uncache: (selector, success, error) ->
compiledSelector = utils.compileDocumentSelector(selector)
for item in _.values(@items)
if not @upserts[item._id]? and compiledSelector(item)
@_deleteItem(item._id)
if success? then success()
uncacheList: (ids, success, error) ->
for id in ids
if not @upserts[id]?
@_deleteItem(id)
if success? then success()