"use strict"; var _CoreManager = _interopRequireDefault(require("./CoreManager")); var _ParseQuery = _interopRequireDefault(require("./ParseQuery")); var _LocalDatastoreUtils = require("./LocalDatastoreUtils"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * @flow */ /*:: import type ParseObject from './ParseObject';*/ /** * Provides a local datastore which can be used to store and retrieve Parse.Object.
* To enable this functionality, call Parse.enableLocalDatastore(). * * Pin object to add to local datastore * *
await object.pin();
*
await object.pinWithName('pinName');
* * Query pinned objects * *
query.fromLocalDatastore();
*
query.fromPin();
*
query.fromPinWithName();
* *
const localObjects = await query.find();
* * @class Parse.LocalDatastore * @static */ const LocalDatastore = { isEnabled: false, isSyncing: false, fromPinWithName(name /*: string*/) /*: Promise>*/{ const controller = _CoreManager.default.getLocalDatastoreController(); return controller.fromPinWithName(name); }, pinWithName(name /*: string*/, value /*: any*/) /*: Promise*/{ const controller = _CoreManager.default.getLocalDatastoreController(); return controller.pinWithName(name, value); }, unPinWithName(name /*: string*/) /*: Promise*/{ const controller = _CoreManager.default.getLocalDatastoreController(); return controller.unPinWithName(name); }, _getAllContents() /*: Promise*/{ const controller = _CoreManager.default.getLocalDatastoreController(); return controller.getAllContents(); }, // Use for testing _getRawStorage() /*: Promise*/{ const controller = _CoreManager.default.getLocalDatastoreController(); return controller.getRawStorage(); }, _clear() /*: Promise*/{ const controller = _CoreManager.default.getLocalDatastoreController(); return controller.clear(); }, // Pin the object and children recursively // Saves the object and children key to Pin Name async _handlePinAllWithName(name /*: string*/, objects /*: Array*/) /*: Promise*/{ const pinName = this.getPinName(name); const toPinPromises = []; const objectKeys = []; for (const parent of objects) { const children = this._getChildren(parent); const parentKey = this.getKeyForObject(parent); const json = parent._toFullJSON(undefined, true); if (parent._localId) { json._localId = parent._localId; } children[parentKey] = json; for (const objectKey in children) { objectKeys.push(objectKey); toPinPromises.push(this.pinWithName(objectKey, [children[objectKey]])); } } const fromPinPromise = this.fromPinWithName(pinName); const [pinned] = await Promise.all([fromPinPromise, toPinPromises]); const toPin = [...new Set([...(pinned || []), ...objectKeys])]; return this.pinWithName(pinName, toPin); }, // Removes object and children keys from pin name // Keeps the object and children pinned async _handleUnPinAllWithName(name /*: string*/, objects /*: Array*/) { const localDatastore = await this._getAllContents(); const pinName = this.getPinName(name); const promises = []; let objectKeys = []; for (const parent of objects) { const children = this._getChildren(parent); const parentKey = this.getKeyForObject(parent); objectKeys.push(parentKey, ...Object.keys(children)); } objectKeys = [...new Set(objectKeys)]; let pinned = localDatastore[pinName] || []; pinned = pinned.filter(item => !objectKeys.includes(item)); if (pinned.length == 0) { promises.push(this.unPinWithName(pinName)); delete localDatastore[pinName]; } else { promises.push(this.pinWithName(pinName, pinned)); localDatastore[pinName] = pinned; } for (const objectKey of objectKeys) { let hasReference = false; for (const key in localDatastore) { if (key === _LocalDatastoreUtils.DEFAULT_PIN || key.startsWith(_LocalDatastoreUtils.PIN_PREFIX)) { const pinnedObjects = localDatastore[key] || []; if (pinnedObjects.includes(objectKey)) { hasReference = true; break; } } } if (!hasReference) { promises.push(this.unPinWithName(objectKey)); } } return Promise.all(promises); }, // Retrieve all pointer fields from object recursively _getChildren(object /*: ParseObject*/) { const encountered = {}; const json = object._toFullJSON(undefined, true); for (const key in json) { if (json[key] && json[key].__type && json[key].__type === 'Object') { this._traverse(json[key], encountered); } } return encountered; }, _traverse(object /*: any*/, encountered /*: any*/) { if (!object.objectId) { return; } else { const objectKey = this.getKeyForObject(object); if (encountered[objectKey]) { return; } encountered[objectKey] = object; } for (const key in object) { let json = object[key]; if (!object[key]) { json = object; } if (json.__type && json.__type === 'Object') { this._traverse(json, encountered); } } }, // Transform keys in pin name to objects async _serializeObjectsFromPinName(name /*: string*/) { const localDatastore = await this._getAllContents(); const allObjects = []; for (const key in localDatastore) { if (key.startsWith(_LocalDatastoreUtils.OBJECT_PREFIX)) { allObjects.push(localDatastore[key][0]); } } if (!name) { return allObjects; } const pinName = this.getPinName(name); const pinned = localDatastore[pinName]; if (!Array.isArray(pinned)) { return []; } const promises = pinned.map(objectKey => this.fromPinWithName(objectKey)); let objects = await Promise.all(promises); objects = [].concat(...objects); return objects.filter(object => object != null); }, // Replaces object pointers with pinned pointers // The object pointers may contain old data // Uses Breadth First Search Algorithm async _serializeObject(objectKey /*: string*/, localDatastore /*: any*/) { let LDS = localDatastore; if (!LDS) { LDS = await this._getAllContents(); } if (!LDS[objectKey] || LDS[objectKey].length === 0) { return null; } const root = LDS[objectKey][0]; const queue = []; const meta = {}; let uniqueId = 0; meta[uniqueId] = root; queue.push(uniqueId); while (queue.length !== 0) { const nodeId = queue.shift(); const subTreeRoot = meta[nodeId]; for (const field in subTreeRoot) { const value = subTreeRoot[field]; if (value.__type && value.__type === 'Object') { const key = this.getKeyForObject(value); if (LDS[key] && LDS[key].length > 0) { const pointer = LDS[key][0]; uniqueId++; meta[uniqueId] = pointer; subTreeRoot[field] = pointer; queue.push(uniqueId); } } } } return root; }, // Called when an object is save / fetched // Update object pin value async _updateObjectIfPinned(object /*: ParseObject*/) /*: Promise*/{ if (!this.isEnabled) { return; } const objectKey = this.getKeyForObject(object); const pinned = await this.fromPinWithName(objectKey); if (!pinned || pinned.length === 0) { return; } return this.pinWithName(objectKey, [object._toFullJSON()]); }, // Called when object is destroyed // Unpin object and remove all references from pin names // TODO: Destroy children? async _destroyObjectIfPinned(object /*: ParseObject*/) { if (!this.isEnabled) { return; } const localDatastore = await this._getAllContents(); const objectKey = this.getKeyForObject(object); const pin = localDatastore[objectKey]; if (!pin) { return; } const promises = [this.unPinWithName(objectKey)]; delete localDatastore[objectKey]; for (const key in localDatastore) { if (key === _LocalDatastoreUtils.DEFAULT_PIN || key.startsWith(_LocalDatastoreUtils.PIN_PREFIX)) { let pinned = localDatastore[key] || []; if (pinned.includes(objectKey)) { pinned = pinned.filter(item => item !== objectKey); if (pinned.length == 0) { promises.push(this.unPinWithName(key)); delete localDatastore[key]; } else { promises.push(this.pinWithName(key, pinned)); localDatastore[key] = pinned; } } } } return Promise.all(promises); }, // Update pin and references of the unsaved object async _updateLocalIdForObject(localId /*: string*/, object /*: ParseObject*/) { if (!this.isEnabled) { return; } const localKey = `${_LocalDatastoreUtils.OBJECT_PREFIX}${object.className}_${localId}`; const objectKey = this.getKeyForObject(object); const unsaved = await this.fromPinWithName(localKey); if (!unsaved || unsaved.length === 0) { return; } const promises = [this.unPinWithName(localKey), this.pinWithName(objectKey, unsaved)]; const localDatastore = await this._getAllContents(); for (const key in localDatastore) { if (key === _LocalDatastoreUtils.DEFAULT_PIN || key.startsWith(_LocalDatastoreUtils.PIN_PREFIX)) { let pinned = localDatastore[key] || []; if (pinned.includes(localKey)) { pinned = pinned.filter(item => item !== localKey); pinned.push(objectKey); promises.push(this.pinWithName(key, pinned)); localDatastore[key] = pinned; } } } return Promise.all(promises); }, /** * Updates Local Datastore from Server * *
   * await Parse.LocalDatastore.updateFromServer();
   * 
* * @function updateFromServer * @name Parse.LocalDatastore.updateFromServer * @static */ async updateFromServer() { if (!this.checkIfEnabled() || this.isSyncing) { return; } const localDatastore = await this._getAllContents(); const keys = []; for (const key in localDatastore) { if (key.startsWith(_LocalDatastoreUtils.OBJECT_PREFIX)) { keys.push(key); } } if (keys.length === 0) { return; } this.isSyncing = true; const pointersHash = {}; for (const key of keys) { // Ignore the OBJECT_PREFIX let [,, className, objectId] = key.split('_'); // User key is split into [ 'Parse', 'LDS', '', 'User', 'objectId' ] if (key.split('_').length === 5 && key.split('_')[3] === 'User') { className = '_User'; objectId = key.split('_')[4]; } if (objectId.startsWith('local')) { continue; } if (!(className in pointersHash)) { pointersHash[className] = new Set(); } pointersHash[className].add(objectId); } const queryPromises = Object.keys(pointersHash).map(className => { const objectIds = Array.from(pointersHash[className]); const query = new _ParseQuery.default(className); query.limit(objectIds.length); if (objectIds.length === 1) { query.equalTo('objectId', objectIds[0]); } else { query.containedIn('objectId', objectIds); } return query.find(); }); try { const responses = await Promise.all(queryPromises); const objects = [].concat.apply([], responses); const pinPromises = objects.map(object => { const objectKey = this.getKeyForObject(object); return this.pinWithName(objectKey, object._toFullJSON()); }); await Promise.all(pinPromises); this.isSyncing = false; } catch (error) { console.error('Error syncing LocalDatastore: ', error); this.isSyncing = false; } }, getKeyForObject(object /*: any*/) { const objectId = object.objectId || object._getId(); return `${_LocalDatastoreUtils.OBJECT_PREFIX}${object.className}_${objectId}`; }, getPinName(pinName /*: ?string*/) { if (!pinName || pinName === _LocalDatastoreUtils.DEFAULT_PIN) { return _LocalDatastoreUtils.DEFAULT_PIN; } return _LocalDatastoreUtils.PIN_PREFIX + pinName; }, checkIfEnabled() { if (!this.isEnabled) { console.error('Parse.enableLocalDatastore() must be called first'); } return this.isEnabled; } }; module.exports = LocalDatastore; _CoreManager.default.setLocalDatastoreController(require('./LocalDatastoreController')); _CoreManager.default.setLocalDatastore(LocalDatastore);