UNPKG

minimongo

Version:

Client-side mongo database with server sync over http

393 lines (392 loc) 17.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HybridCollection = void 0; const lodash_1 = __importDefault(require("lodash")); const utils_1 = require("./utils"); /** Bridges a local and remote database, querying from the local first and then * getting the remote. Also uploads changes from local to remote. */ class HybridDb { constructor(localDb, remoteDb) { this.localDb = localDb; this.remoteDb = remoteDb; this.collections = {}; } addCollection(name, options, success, error) { // Shift options over if not present if (lodash_1.default.isFunction(options)) { ; [options, success, error] = [{}, options, success]; } const collection = new HybridCollection(name, this.localDb[name], this.remoteDb[name], options); this[name] = collection; this.collections[name] = collection; if (success != null) { return success(); } } removeCollection(name, success, error) { delete this[name]; delete this.collections[name]; if (success != null) { return success(); } } upload(success, error) { if (success == null) { return new Promise((resolve, reject) => { return this.upload(resolve, reject); }); } const cols = Object.values(this.collections); function uploadCols(cols, success, error) { const col = lodash_1.default.first(cols); if (col) { col.upload(() => uploadCols(lodash_1.default.tail(cols), success, error), (err) => error(err)); } else { success(); } } return uploadCols(cols, success, error); } getCollectionNames() { return lodash_1.default.keys(this.collections); } } exports.default = HybridDb; class HybridCollection { // Options includes constructor(name, localCol, remoteCol, options) { this.name = name; this.localCol = localCol; this.remoteCol = remoteCol; // Default options this.options = options || {}; lodash_1.default.defaults(this.options, { cacheFind: true, cacheFindOne: true, interim: true, useLocalOnRemoteError: true, shortcut: false, timeout: 0, sortUpserts: null // Compare function to sort upserts sent to server }); } 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, Object.assign(Object.assign({}, options), { interim: false }), resolve, reject); }); } // Merge options lodash_1.default.defaults(options, this.options); // Happens after initial find const step2 = (localDoc) => { const findOptions = Object.assign({}, options); findOptions.interim = false; findOptions.cacheFind = options.cacheFindOne; if (selector._id) { findOptions.limit = 1; } else { // Without _id specified, interaction between local and remote changes is complex // For example, if the one result returned by remote is locally deleted, we have no fallback // So instead we do a find with no limit and then take the first result, which is very inefficient delete findOptions.limit; } return this.find(selector, findOptions).fetch(function (data) { // Return first entry or null if (data.length > 0) { // Check that different from existing if (JSON.stringify(localDoc) != JSON.stringify(data[0])) { return success(data[0]); } } else { // If nothing found, always report it, as interim find doesn't return null return success(null); } }, error); }; // If interim or shortcut, get local first if (options.interim || options.shortcut) { return this.localCol.findOne(selector, options, function (localDoc) { // If found, return if (localDoc) { success(JSON.parse(JSON.stringify(localDoc))); // If shortcut, we're done if (options.shortcut) { return; } } return step2(localDoc); }, error); } else { return step2(null); } } _findFetch(selector, options, success, error) { // If promise case if (success == null) { // Implies interim false (since promises cannot resolve twice) return new Promise((resolve, reject) => { this._findFetch(selector, Object.assign(Object.assign({}, options), { interim: false }), resolve, reject); }); } // Merge options lodash_1.default.defaults(options, this.options); // Get pending removes and upserts immediately to avoid odd race conditions this.localCol.pendingUpserts((upserts) => { this.localCol.pendingRemoves((removes) => { const step2 = (localData) => { // Setup remote options const remoteOptions = Object.assign({}, options); // If caching, get all fields if (options.cacheFind) { delete remoteOptions.fields; } // Add localData to options for remote find for quickfind protocol remoteOptions.localData = localData; // Setup timer variables let timer = null; let timedOut = false; const remoteSuccess = (remoteData) => { // Cancel timer if (timer) { clearTimeout(timer); } // Ignore if timed out, caching asynchronously if (timedOut) { if (options.cacheFind) { this.localCol.cache(remoteData, selector, options, function () { }, error); } return; } if (options.cacheFind) { // Cache locally const cacheSuccess = () => { // Get local data again function localSuccess2(localData2) { // Check if different or not interim if (!options.interim || JSON.stringify(localData) != JSON.stringify(localData2)) { // Send again return success(localData2); } } return this.localCol.find(selector, options).fetch(localSuccess2, error); }; // Exclude any recent upserts/removes to prevent race condition const cacheOptions = lodash_1.default.extend({}, options, { exclude: removes.concat(lodash_1.default.map(upserts, (u) => u.doc._id)) }); return this.localCol.cache(remoteData, selector, cacheOptions, cacheSuccess, error); } else { // Remove local remotes let data = remoteData; if (removes.length > 0) { const removesMap = lodash_1.default.fromPairs(lodash_1.default.map(removes, (id) => [id, id])); data = lodash_1.default.filter(remoteData, (doc) => !lodash_1.default.has(removesMap, doc._id)); } // Add upserts if (upserts.length > 0) { // Remove upserts from data const upsertsMap = lodash_1.default.fromPairs(lodash_1.default.zip(lodash_1.default.map(upserts, (u) => u.doc._id), lodash_1.default.map(upserts, (u) => u.doc._id))); data = lodash_1.default.filter(data, (doc) => !lodash_1.default.has(upsertsMap, doc._id)); // Add upserts data = data.concat(lodash_1.default.map(upserts, "doc")); // Refilter/sort/limit data = (0, utils_1.processFind)(data, selector, options); } // Check if different or not interim if (!options.interim || JSON.stringify(localData) != JSON.stringify(data)) { // Send again return success(data); } } }; const remoteError = (err) => { // Cancel timer if (timer) { clearTimeout(timer); } if (timedOut) { return; } // If no interim, do local find if (!options.interim) { if (options.useLocalOnRemoteError) { return success(localData); } else { if (error) { return error(err); } } } else { // Otherwise do nothing return; } }; // Start timer if remote if (options.timeout) { timer = setTimeout(() => { timer = null; timedOut = true; // If no interim, do local find if (!options.interim) { if (options.useLocalOnRemoteError) { return this.localCol.find(selector, options).fetch(success, error); } else { if (error) { return error(new Error("Remote timed out")); } } } else { // Otherwise do nothing return; } }, options.timeout); } return this.remoteCol.find(selector, remoteOptions).fetch(remoteSuccess, remoteError); }; function localSuccess(localData) { // If interim, return data immediately if (options.interim) { success(localData); } return step2(localData); } // Always get local data first return this.localCol.find(selector, options).fetch(localSuccess, error); }, error); }, error); } upsert(docs, bases, success, error) { if (!success && !lodash_1.default.isFunction(bases)) { return new Promise((resolve, reject) => { this.upsert(docs, bases, resolve, reject); }); } return this.localCol.upsert(docs, bases, success, error); } remove(id, success, error) { if (!success) { return new Promise((resolve, reject) => { this.remove(id, resolve, reject); }); } return this.localCol.remove(id, function () { if (success != null) { return success(); } }, error); } upload(success, error) { const uploadUpserts = (upserts, success, error) => { const upsert = lodash_1.default.first(upserts); if (upsert) { // Handle case if identical doc and base https://github.com/mWater/minimongo/issues/89 if (JSON.stringify(upsert.doc) === JSON.stringify(upsert.base)) { return this.localCol.resolveUpserts([upsert], () => uploadUpserts(lodash_1.default.tail(upserts), success, error), error); } return this.remoteCol.upsert(upsert.doc, upsert.base, (remoteDoc) => { return this.localCol.resolveUpserts([upsert], () => { // Cache new value if present if (remoteDoc) { return this.localCol.cacheOne(remoteDoc, () => uploadUpserts(lodash_1.default.tail(upserts), success, error), error); } else { // Remove local return this.localCol.remove(upsert.doc._id, () => { // Resolve remove return this.localCol.resolveRemove(upsert.doc._id, () => uploadUpserts(lodash_1.default.tail(upserts), success, error), error); }, error); } }, error); }, (err) => { // If 410 error or 403, remove document if (err.status === 410 || err.status === 403) { return this.localCol.remove(upsert.doc._id, () => { // Resolve remove return this.localCol.resolveRemove(upsert.doc._id, function () { // Continue if was 410 if (err.status === 410) { return uploadUpserts(lodash_1.default.tail(upserts), success, error); } else { return error(err); } }, error); }, error); } else { return error(err); } }); } else { return success(); } }; const uploadRemoves = (removes, success, error) => { const remove = lodash_1.default.first(removes); if (remove) { return this.remoteCol.remove(remove, () => { return this.localCol.resolveRemove(remove, () => uploadRemoves(lodash_1.default.tail(removes), success, error), error); }, (err) => { // If 403 or 410, remove document if (err.status === 410 || err.status === 403) { return this.localCol.resolveRemove(remove, function () { // Continue if was 410 if (err.status === 410) { return uploadRemoves(lodash_1.default.tail(removes), success, error); } else { return error(err); } }, error); } else { return error(err); } }); } else { success(); } }; // Get pending upserts this.localCol.pendingUpserts((upserts) => { // Sort upserts if sort defined if (this.options.sortUpserts) { upserts.sort((u1, u2) => this.options.sortUpserts(u1.doc, u2.doc)); } return uploadUpserts(upserts, () => { return this.localCol.pendingRemoves((removes) => uploadRemoves(removes, success, error), error); }, error); }, error); } } exports.HybridCollection = HybridCollection;