minimongo
Version:
Client-side mongo database with server sync over http
382 lines (381 loc) • 14.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = __importDefault(require("lodash"));
const async_1 = __importDefault(require("async"));
const utils = __importStar(require("./utils"));
const utils_1 = require("./utils");
const selector_1 = require("./selector");
class LocalStorageDb {
constructor(options, success, error) {
this.collections = {};
if (options && options.namespace && window.localStorage) {
this.namespace = options.namespace;
}
if (success) {
success(this);
}
}
addCollection(name, success, error) {
// 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, success, error) {
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) {
const keyToMatch = this.namespace + "." + name;
if (key.substring(0, keyToMatch.length) === keyToMatch) {
window.localStorage.removeItem(key);
}
}
}
delete this[name];
delete this.collections[name];
if (success != null) {
return success();
}
}
getCollectionNames() {
return lodash_1.default.keys(this.collections);
}
}
exports.default = LocalStorageDb;
// Stores data in memory, optionally backed by local storage
class Collection {
constructor(name, namespace) {
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] = { doc: this.items[key] };
// Get base if present
const base = window.localStorage[this.namespace + "upsertbase_" + key]
? JSON.parse(window.localStorage[this.namespace + "upsertbase_" + key])
: null;
this.upserts[key].base = base;
}
// Read removes
const removeItems = window.localStorage[this.namespace + "removes"]
? JSON.parse(window.localStorage[this.namespace + "removes"])
: [];
return (this.removes = lodash_1.default.fromPairs(lodash_1.default.zip(lodash_1.default.map(removeItems, "_id"), removeItems)));
}
find(selector, options) {
return {
fetch: (success, error) => {
return this._findFetch(selector, options, success, error);
}
};
}
findOne(selector, options, success, error) {
if (lodash_1.default.isFunction(options)) {
;
[options, success, error] = [{}, options, success];
}
options = options || {};
// If promise case
if (success == null) {
return new Promise((resolve, reject) => {
this.findOne(selector, options, resolve, reject);
});
}
return this.find(selector, options).fetch(function (results) {
if (success != null) {
return success(results.length > 0 ? results[0] : null);
}
}, error);
}
_findFetch(selector, options, success, error) {
// If promise case
if (success == null) {
return new Promise((resolve, reject) => {
this._findFetch(selector, options, resolve, reject);
});
}
// Deep clone to prevent modification
if (success != null) {
return success((0, utils_1.processFind)(JSON.parse(JSON.stringify(lodash_1.default.values(this.items))), selector, options));
}
}
upsert(docs, bases, success, error) {
// If promise case
if (!success && !lodash_1.default.isFunction(bases)) {
return new Promise((resolve, reject) => {
this.upsert(docs, bases, resolve, reject);
});
}
let items;
[items, success, error] = utils.regularizeUpsert(docs, bases, success, error);
// Keep independent copies to prevent modification
items = JSON.parse(JSON.stringify(items));
for (let item of items) {
// Fill in base
if (item.base === undefined) {
// Use existing base
if (this.upserts[item.doc._id]) {
item.base = this.upserts[item.doc._id].base;
}
else {
item.base = this.items[item.doc._id] || null;
}
}
// Keep independent copies
item = JSON.parse(JSON.stringify(item));
// Replace/add
this._putItem(item.doc);
this._putUpsert(item);
}
if (success) {
return success(docs);
}
}
remove(id, success, error) {
if (!success) {
return new Promise((resolve, reject) => {
this.remove(id, resolve, reject);
});
}
// Special case for filter-type remove
if (lodash_1.default.isObject(id)) {
this.find(id).fetch((rows) => {
return async_1.default.each(rows, ((row, cb) => {
return this.remove(row._id, () => cb(), cb);
}), () => success());
}, error);
return;
}
if (lodash_1.default.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) {
this.items[doc._id] = doc;
if (this.namespace) {
window.localStorage[this.itemNamespace + doc._id] = JSON.stringify(doc);
}
}
_deleteItem(id) {
delete this.items[id];
if (this.namespace) {
window.localStorage.removeItem(this.itemNamespace + id);
}
}
_putUpsert(upsert) {
this.upserts[upsert.doc._id] = upsert;
if (this.namespace) {
window.localStorage[this.namespace + "upserts"] = JSON.stringify(lodash_1.default.keys(this.upserts));
window.localStorage[this.namespace + "upsertbase_" + upsert.doc._id] = JSON.stringify(upsert.base);
}
}
_deleteUpsert(id) {
delete this.upserts[id];
if (this.namespace) {
window.localStorage[this.namespace + "upserts"] = JSON.stringify(lodash_1.default.keys(this.upserts));
}
}
_putRemove(doc) {
this.removes[doc._id] = doc;
if (this.namespace) {
window.localStorage[this.namespace + "removes"] = JSON.stringify(lodash_1.default.values(this.removes));
}
}
_deleteRemove(id) {
delete this.removes[id];
if (this.namespace) {
window.localStorage[this.namespace + "removes"] = JSON.stringify(lodash_1.default.values(this.removes));
}
}
cache(docs, selector, options, success, error) {
// Add all non-local that are not upserted or removed
let sort;
for (let doc of docs) {
// Exclude any excluded _ids from being cached/uncached
if (options && options.exclude && options.exclude.includes(doc._id)) {
continue;
}
this.cacheOne(doc, () => { }, () => { });
}
const docsMap = lodash_1.default.fromPairs(lodash_1.default.zip(lodash_1.default.map(docs, "_id"), docs));
if (options.sort) {
sort = (0, selector_1.compileSort)(options.sort);
}
// Perform query, removing rows missing in docs from local db
return this.find(selector, options).fetch((results) => {
for (let result of results) {
if (!docsMap[result._id] && !lodash_1.default.has(this.upserts, result._id)) {
// Exclude any excluded _ids from being cached/uncached
if (options && options.exclude && options.exclude.includes(result._id)) {
continue;
}
// If at limit
if (options.limit && docs.length === options.limit) {
// If past end on sorted limited, ignore
if (options.sort && sort(result, lodash_1.default.last(docs)) >= 0) {
continue;
}
// If no sort, ignore
if (!options.sort) {
continue;
}
}
this._deleteItem(result._id);
}
}
if (success != null) {
return success();
}
}, error);
}
pendingUpserts(success) {
return success(lodash_1.default.values(this.upserts));
}
pendingRemoves(success) {
return success(lodash_1.default.map(this.removes, "_id"));
}
resolveUpserts(upserts, success) {
for (let upsert of upserts) {
if (this.upserts[upsert.doc._id]) {
// Only safely remove upsert if item is unchanged
if (JSON.stringify(upsert.doc) == JSON.stringify(this.upserts[upsert.doc._id].doc)) {
this._deleteUpsert(upsert.doc._id);
}
else {
// Just update base
this.upserts[upsert.doc._id].base = upsert.doc;
this._putUpsert(this.upserts[upsert.doc._id]);
}
}
}
if (success != null) {
return success();
}
}
resolveRemove(id, success) {
this._deleteRemove(id);
if (success != null) {
return success();
}
}
// Add but do not overwrite or record as upsert
seed(docs, success) {
if (!lodash_1.default.isArray(docs)) {
docs = [docs];
}
for (let doc of docs) {
if (!lodash_1.default.has(this.items, doc._id) && !lodash_1.default.has(this.removes, doc._id)) {
this._putItem(doc);
}
}
if (success != null) {
return success();
}
}
// Add but do not overwrite upserts or removes
cacheOne(doc, success, error) {
return this.cacheList([doc], success, error);
}
// Add but do not overwrite upserts or removes
cacheList(docs, success, error) {
for (let doc of docs) {
if (!lodash_1.default.has(this.upserts, doc._id) && !lodash_1.default.has(this.removes, doc._id)) {
const existing = this.items[doc._id];
// If _rev present, make sure that not overwritten by lower or equal _rev
if (!existing || !doc._rev || !existing._rev || doc._rev > existing._rev) {
this._putItem(doc);
}
}
}
if (success != null) {
return success();
}
}
uncache(selector, success, error) {
const compiledSelector = utils.compileDocumentSelector(selector);
for (let item of lodash_1.default.values(this.items)) {
if (this.upserts[item._id] == null && compiledSelector(item)) {
this._deleteItem(item._id);
}
}
if (success != null) {
return success();
}
}
uncacheList(ids, success, error) {
for (let id of ids) {
if (this.upserts[id] == null) {
this._deleteItem(id);
}
}
if (success != null) {
return success();
}
}
}