minimongo
Version:
Client-side mongo database with server sync over http
300 lines (255 loc) • 7.9 kB
text/typescript
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
let LocalStorageDb
import _ from "lodash"
import { createUid, processFind } from "./utils"
import { compileSort } from "./selector"
export default LocalStorageDb = class LocalStorageDb {
constructor(options: any, success: any) {
this.collections = {}
if (options && options.namespace && window.localStorage) {
this.namespace = options.namespace
}
if (success) {
success(this)
}
}
addCollection(name: any, success: any, error: any) {
// Set namespace for collection
let namespace
if (this.namespace) {
namespace = this.namespace + "." + name
}
const collection = new Collection(name, namespace)
this[name] = collection
this.collections[name] = collection
if (success != null) {
return success()
}
}
removeCollection(name: any, success: any, error: any) {
if (this.namespace && window.localStorage) {
const keys = []
for (let i = 0, end = window.localStorage.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
keys.push(window.localStorage.key(i))
}
for (let key of keys) {
if (key.substring(0, this.namespace.length + 1) === this.namespace + ".") {
window.localStorage.removeItem(key)
}
}
}
delete this[name]
delete this.collections[name]
if (success != null) {
return success()
}
}
}
// Stores data in memory, optionally backed by local storage
class Collection {
constructor(name: any, namespace: any) {
this.name = name
this.namespace = namespace
this.items = {}
this.upserts = {} // Pending upserts by _id. Still in items
this.removes = {} // Pending removes by _id. No longer in items
// Read from local storage
if (window.localStorage && namespace != null) {
this.loadStorage()
}
}
loadStorage() {
// Read items from localStorage
let key
this.itemNamespace = this.namespace + "_"
for (let i = 0, end = window.localStorage.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
key = window.localStorage.key(i)
if (key.substring(0, this.itemNamespace.length) === this.itemNamespace) {
const item = JSON.parse(window.localStorage[key])
this.items[item._id] = item
}
}
// Read upserts
const upsertKeys = window.localStorage[this.namespace + "upserts"]
? JSON.parse(window.localStorage[this.namespace + "upserts"])
: []
for (key of upsertKeys) {
this.upserts[key] = this.items[key]
}
// Read removes
const removeItems = window.localStorage[this.namespace + "removes"]
? JSON.parse(window.localStorage[this.namespace + "removes"])
: []
return (this.removes = _.fromPairs(_.zip(_.map(removeItems, "_id"), removeItems)))
}
find(selector: any, options: any) {
return {
fetch: (success: any, error: any) => {
return this._findFetch(selector, options, success, error)
}
}
}
findOne(selector: any, options: any, success: any, error: any) {
if (_.isFunction(options)) {
;[options, success, error] = [{}, options, success]
}
return this.find(selector, options).fetch(function (results: any) {
if (success != null) {
return success(results.length > 0 ? results[0] : null)
}
}, error)
}
_findFetch(selector: any, options: any, success: any, error: any) {
if (success != null) {
return success(processFind(this.items, selector, options))
}
}
upsert(doc: any, success: any, error: any) {
// Handle both single and multiple upsert
let items = doc
if (!_.isArray(items)) {
items = [items]
}
// Handle case of array
for (let item of items) {
if (!item._id) {
item._id = createUid()
}
// Replace/add
this._putItem(item)
this._putUpsert(item)
}
if (success) {
return success(doc)
}
}
remove(id: any, success: any, error: any) {
if (_.has(this.items, id)) {
this._putRemove(this.items[id])
this._deleteItem(id)
this._deleteUpsert(id)
} else {
this._putRemove({ _id: id })
}
if (success != null) {
return success()
}
}
_putItem(doc: any) {
this.items[doc._id] = doc
if (this.namespace) {
return (window.localStorage[this.itemNamespace + doc._id] = JSON.stringify(doc))
}
}
_deleteItem(id: any) {
delete this.items[id]
if (this.namespace) {
return window.localStorage.removeItem(this.itemNamespace + id)
}
}
_putUpsert(doc: any) {
this.upserts[doc._id] = doc
if (this.namespace) {
return (window.localStorage[this.namespace + "upserts"] = JSON.stringify(_.keys(this.upserts)))
}
}
_deleteUpsert(id: any) {
delete this.upserts[id]
if (this.namespace) {
return (window.localStorage[this.namespace + "upserts"] = JSON.stringify(_.keys(this.upserts)))
}
}
_putRemove(doc: any) {
this.removes[doc._id] = doc
if (this.namespace) {
return (window.localStorage[this.namespace + "removes"] = JSON.stringify(_.values(this.removes)))
}
}
_deleteRemove(id: any) {
delete this.removes[id]
if (this.namespace) {
return (window.localStorage[this.namespace + "removes"] = JSON.stringify(_.values(this.removes)))
}
}
cache(docs: any, selector: any, options: any, success: any, error: any) {
// Add all non-local that are not upserted or removed
let sort: any
for (let doc of docs) {
this.cacheOne(doc)
}
const docsMap = _.fromPairs(_.zip(_.map(docs, "_id"), docs))
if (options.sort) {
sort = compileSort(options.sort)
}
// Perform query, removing rows missing in docs from local db
return this.find(selector, options).fetch((results: any) => {
for (let result of results) {
if (!docsMap[result._id] && !_.has(this.upserts, result._id)) {
// If past end on sorted limited, ignore
if (options.sort && options.limit && docs.length === options.limit) {
if (sort(result, _.last(docs)) >= 0) {
continue
}
}
this._deleteItem(result._id)
}
}
if (success != null) {
return success()
}
}, error)
}
pendingUpserts(success: any) {
return success(_.values(this.upserts))
}
pendingRemoves(success: any) {
return success(_.map(this.removes, "_id"))
}
resolveUpsert(doc: any, success: any) {
// Handle both single and multiple upsert
let items = doc
if (!_.isArray(items)) {
items = [items]
}
for (let item of items) {
if (this.upserts[item._id]) {
// Only safely remove upsert if item is unchanged
if (_.isEqual(item, this.upserts[item._id])) {
this._deleteUpsert(item._id)
}
}
}
if (success != null) {
return success()
}
}
resolveRemove(id: any, success: any) {
this._deleteRemove(id)
if (success != null) {
return success()
}
}
// Add but do not overwrite or record as upsert
seed(doc: any, success: any) {
if (!_.has(this.items, doc._id) && !_.has(this.removes, doc._id)) {
this._putItem(doc)
}
if (success != null) {
return success()
}
}
// Add but do not overwrite upserts or removes
cacheOne(doc: any, success: any) {
if (!_.has(this.upserts, doc._id) && !_.has(this.removes, doc._id)) {
const existing = this.items[doc._id]
// If _rev present, make sure that not overwritten by lower _rev
if (!existing || !doc._rev || !existing._rev || doc._rev >= existing._rev) {
this._putItem(doc)
}
}
if (success != null) {
return success()
}
}
}