UNPKG

minimongo

Version:

Client-side mongo database with server sync over http

508 lines (507 loc) 20.1 kB
"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 idb_wrapper_1 = __importDefault(require("idb-wrapper")); const utils = __importStar(require("./utils")); const utils_1 = require("./utils"); const selector_1 = require("./selector"); // Create a database backed by IndexedDb. options must contain namespace: <string to uniquely identify database> class IndexedDb { constructor(options, success, error) { this.collections = {}; // Create database try { this.store = new idb_wrapper_1.default({ dbVersion: 1, storeName: "minimongo_" + options.namespace, keyPath: ["col", "doc._id"], autoIncrement: false, onStoreReady: () => { if (success) { return success(this); } }, onError: error, indexes: [ { name: "col", keyPath: "col", unique: false, multiEntry: false }, { name: "col-state", keyPath: ["col", "state"], unique: false, multiEntry: false } ] }); } catch (ex) { if (error) { error(ex); } return; } } addCollection(name, success, error) { const collection = new IndexedDbCollection(name, this.store); this[name] = collection; this.collections[name] = collection; if (success) { return success(); } } removeCollection(name, success, error) { delete this[name]; delete this.collections[name]; // Remove all documents return this.store.query((matches) => { const keys = lodash_1.default.map(matches, (m) => [m.col, m.doc._id]); if (keys.length > 0) { return this.store.removeBatch(keys, function () { if (success != null) { return success(); } }, error); } else { if (success != null) { return success(); } } }, { index: "col", keyRange: this.store.makeKeyRange({ only: name }), onError: error }); } getCollectionNames() { return lodash_1.default.keys(this.collections); } } exports.default = IndexedDb; // Stores data in indexeddb store class IndexedDbCollection { constructor(name, store) { this.name = name; this.store = store; } 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); }); } this.find(selector, options).fetch(function (results) { if (success != null) { success(results.length > 0 ? results[0] : null); } }, error); return; } _findFetch(selector, options, success, error) { // If promise case if (success == null) { return new Promise((resolve, reject) => { this._findFetch(selector, options, resolve, reject); }); } // Get all docs from collection return this.store.query(function (matches) { // Filter removed docs matches = lodash_1.default.filter(matches, (m) => m.state !== "removed"); if (success != null) { return success((0, utils_1.processFind)(lodash_1.default.map(matches, "doc"), selector, options)); } }, { index: "col", keyRange: this.store.makeKeyRange({ only: this.name }), onError: error }); } 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); // Get bases const keys = lodash_1.default.map(items, (item) => [this.name, item.doc._id]); return this.store.getBatch(keys, (records) => { const puts = lodash_1.default.map(items, (item, i) => { // Prefer explicit base let base; if (item.base !== undefined) { ; ({ base } = item); } else if (records[i] && records[i].doc && records[i].state === "cached") { base = records[i].doc; } else if (records[i] && records[i].doc && records[i].state === "upserted") { ; ({ base } = records[i]); } else { base = null; } return { col: this.name, state: "upserted", doc: item.doc, base }; }); return this.store.putBatch(puts, function () { if (success) { return success(docs); } }, error); }, error); } 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) => { this.remove(row._id, () => cb(), cb); }), () => success()); }, error); return; } // Find record return this.store.get([this.name, id], (record) => { // If not found, create placeholder record if (record == null) { record = { col: this.name, doc: { _id: id } }; } // Set removed record.state = "removed"; // Update return this.store.put(record, function () { if (success) { return success(); } }, error); }); } cache(docs, selector, options, success, error) { const step2 = () => { // Rows have been cached, now look for stale ones to remove let sort; 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) => { const removes = []; const keys = lodash_1.default.map(results, (result) => [this.name, result._id]); if (keys.length === 0) { if (success != null) { success(); } return; } return this.store.getBatch(keys, (records) => { for (let i = 0, end = records.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { const record = records[i]; const result = results[i]; // If not present in docs and is present locally and not upserted/deleted if (!docsMap[result._id] && record && record.state === "cached") { // 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; } } // Exclude any excluded _ids from being cached/uncached if (options && options.exclude && options.exclude.includes(result._id)) { continue; } // Item is gone from server, remove locally removes.push([this.name, result._id]); } } // If removes, handle them if (removes.length > 0) { return this.store.removeBatch(removes, function () { if (success != null) { return success(); } }, error); } else { if (success != null) { return success(); } } }, error); }, error); }; if (docs.length === 0) { return step2(); } // Create keys to get items const keys = lodash_1.default.map(docs, (doc) => [this.name, doc._id]); // Create batch of puts const puts = []; return this.store.getBatch(keys, (records) => { // Add all non-local that are not upserted or removed for (let i = 0, end = records.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { const record = records[i]; const doc = docs[i]; // Check if not present or not upserted/deleted if (record == null || record.state === "cached") { if (options && options.exclude && options.exclude.includes(doc._id)) { continue; } // If _rev present, make sure that not overwritten by lower or equal _rev if (!record || !doc._rev || !record.doc._rev || doc._rev > record.doc._rev) { puts.push({ col: this.name, state: "cached", doc }); } } } // Put batch if (puts.length > 0) { return this.store.putBatch(puts, step2, error); } else { return step2(); } }, error); } pendingUpserts(success, error) { return this.store.query(function (matches) { const upserts = lodash_1.default.map(matches, (m) => ({ doc: m.doc, base: m.base || null })); if (success != null) { return success(upserts); } }, { index: "col-state", keyRange: this.store.makeKeyRange({ only: [this.name, "upserted"] }), onError: error }); } pendingRemoves(success, error) { return this.store.query(function (matches) { if (success != null) { return success(lodash_1.default.map(lodash_1.default.map(matches, "doc"), "_id")); } }, { index: "col-state", keyRange: this.store.makeKeyRange({ only: [this.name, "removed"] }), onError: error }); } resolveUpserts(upserts, success, error) { // Get items const keys = lodash_1.default.map(upserts, (upsert) => [this.name, upsert.doc._id]); return this.store.getBatch(keys, (records) => { const puts = []; for (let i = 0, end = upserts.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { const record = records[i]; // Only safely remove upsert if doc is the same if (record && record.state === "upserted") { if (JSON.stringify(record.doc) == JSON.stringify(upserts[i].doc)) { record.state = "cached"; puts.push(record); } else { record.base = upserts[i].doc; puts.push(record); } } } // Put all changed items if (puts.length > 0) { return this.store.putBatch(puts, function () { if (success) { return success(); } }, error); } else { if (success) { return success(); } } }, error); } resolveRemove(id, success, error) { return this.store.get([this.name, id], (record) => { // Check if exists if (!record) { if (success != null) { success(); } return; } // Only remove if removed if (record.state === "removed") { return this.store.remove([this.name, id], function () { if (success != null) { return success(); } }, error); } }); } // Add but do not overwrite or record as upsert seed(docs, success, error) { if (!lodash_1.default.isArray(docs)) { docs = [docs]; } // Create keys to get items const keys = lodash_1.default.map(docs, (doc) => [this.name, doc._id]); // Create batch of puts const puts = []; return this.store.getBatch(keys, (records) => { // Add all non-local that are not upserted or removed for (let i = 0, end = records.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { const record = records[i]; const doc = docs[i]; // Check if not present if (record == null) { puts.push({ col: this.name, state: "cached", doc }); } } // Put batch if (puts.length > 0) { return this.store.putBatch(puts, () => { if (success != null) { return success(); } }, error); } else { if (success != null) { return success(); } } }, error); } // Add but do not overwrite upsert/removed and do not record as upsert cacheOne(doc, success, error) { return this.cacheList([doc], success, error); } cacheList(docs, success, error) { // Create keys to get items const keys = lodash_1.default.map(docs, (doc) => [this.name, doc._id]); // Create batch of puts const puts = []; return this.store.getBatch(keys, (records) => { for (let i = 0, end = records.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) { let record = records[i]; const doc = docs[i]; // If _rev present, make sure that not overwritten by lower equal _rev if (record && doc._rev && record.doc._rev && doc._rev <= record.doc._rev) { continue; } if (record == null) { record = { col: this.name, state: "cached", doc }; } if (record.state === "cached") { record.doc = doc; puts.push(record); } } // Put batch if (puts.length > 0) { return this.store.putBatch(puts, () => { if (success != null) { return success(); } }, error); } else { if (success != null) { return success(); } } }, error); } uncache(selector, success, error) { const compiledSelector = utils.compileDocumentSelector(selector); // Get all docs from collection return this.store.query((matches) => { // Filter ones to remove matches = lodash_1.default.filter(matches, (m) => m.state === "cached" && compiledSelector(m.doc)); const keys = lodash_1.default.map(matches, (m) => [this.name, m.doc._id]); if (keys.length > 0) { return this.store.removeBatch(keys, () => { if (success != null) { return success(); } }, error); } else { if (success != null) { return success(); } } }, { index: "col", keyRange: this.store.makeKeyRange({ only: this.name }), onError: error }); } uncacheList(ids, success, error) { const idIndex = lodash_1.default.keyBy(ids); // Android 2.x requires error callback error = error || function () { }; // Get all docs from collection return this.store.query((matches) => { // Filter ones to remove matches = lodash_1.default.filter(matches, (m) => m.state === "cached" && idIndex[m.doc._id]); const keys = lodash_1.default.map(matches, (m) => [this.name, m.doc._id]); if (keys.length > 0) { return this.store.removeBatch(keys, () => { if (success != null) { return success(); } }, error); } else { if (success != null) { return success(); } } }, { index: "col", keyRange: this.store.makeKeyRange({ only: this.name }), onError: error }); } }