Skip to content

LocalDatastore fixes for React-Native #753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"prefer-const": "error",
"space-infix-ops": "error",
"no-useless-escape": "off",
"no-var": "error"
"no-var": "error",
"no-console": 0
}
}
402 changes: 226 additions & 176 deletions integration/test/ParseLocalDatastoreTest.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions integration/test/mockRNStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ const mockRNStorage = {
cb(undefined, Object.keys(mockStorage));
},

multiGet(keys, cb) {
const objects = keys.map((key) => [key, mockStorage[key]]);
cb(undefined, objects);
},

multiRemove(keys, cb) {
keys.map((key) => delete mockStorage[key]);
cb(undefined);
},

clear() {
mockStorage = {};
},
Expand Down
7 changes: 3 additions & 4 deletions src/.flowconfig
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
[ignore]
.*/node_modules/.*
.*/node_modules/
.*/lib/

[include]
../package.json

[libs]
interfaces/

[options]
unsafe.enable_getters_and_setters=true
suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next
151 changes: 98 additions & 53 deletions src/LocalDatastore.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,28 @@ import CoreManager from './CoreManager';

import type ParseObject from './ParseObject';
import ParseQuery from './ParseQuery';
import { DEFAULT_PIN, PIN_PREFIX, OBJECT_PREFIX } from './LocalDatastoreUtils';

const DEFAULT_PIN = '_default';
const PIN_PREFIX = 'parsePin_';

/**
* Provides a local datastore which can be used to store and retrieve <code>Parse.Object</code>. <br />
* To enable this functionality, call <code>Parse.enableLocalDatastore()</code>.
*
* Pin object to add to local datastore
*
* <pre>await object.pin();</pre>
* <pre>await object.pinWithName('pinName');</pre>
*
* Query pinned objects
*
* <pre>query.fromLocalDatastore();</pre>
* <pre>query.fromPin();</pre>
* <pre>query.fromPinWithName();</pre>
*
* <pre>const localObjects = await query.find();</pre>
*
* @class Parse.LocalDatastore
* @static
*/
const LocalDatastore = {
fromPinWithName(name: string): Promise {
const controller = CoreManager.getLocalDatastoreController();
Expand All @@ -38,44 +56,62 @@ const LocalDatastore = {
return controller.getAllContents();
},

// Use for testing
_getRawStorage(): Promise {
const controller = CoreManager.getLocalDatastoreController();
return controller.getRawStorage();
},

_clear(): Promise {
const controller = CoreManager.getLocalDatastoreController();
return controller.clear();
},

// Pin the object and children recursively
// Saves the object and children key to Pin Name
async _handlePinWithName(name: string, object: ParseObject): Promise {
async _handlePinAllWithName(name: string, objects: Array<ParseObject>): Promise {
const pinName = this.getPinName(name);
const objects = this._getChildren(object);
objects[this.getKeyForObject(object)] = object._toFullJSON();
for (const objectKey in objects) {
await this.pinWithName(objectKey, objects[objectKey]);
const toPinPromises = [];
const objectKeys = [];
for (const parent of objects) {
const children = this._getChildren(parent);
const parentKey = this.getKeyForObject(parent);
children[parentKey] = parent._toFullJSON();
for (const objectKey in children) {
objectKeys.push(objectKey);
toPinPromises.push(this.pinWithName(objectKey, [children[objectKey]]));
}
}
const pinned = await this.fromPinWithName(pinName) || [];
const objectIds = Object.keys(objects);
const toPin = [...new Set([...pinned, ...objectIds])];
await this.pinWithName(pinName, toPin);
const fromPinPromise = this.fromPinWithName(pinName);
const [pinned] = await Promise.all([fromPinPromise, toPinPromises]);
const toPin = [...new Set([...(pinned || []), ...objectKeys])];
return this.pinWithName(pinName, toPin);
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take 2 at using reduce here, this time with your code. Untested as I'm not setup to test. Just for fun,

async _handlePinAllWithName(name: string, objects: Array<ParseObject>): Promise {
  const pinName = this.getPinName(name);
  const toPinPromises = objects.reduce((accumulator, parent) => {
    const toPin = this._getChildren(parent); // get an object with the children by key
    const parentKey = this.getKeyForObject(parent);
    toPin[parentKey] = parent._toFullJSON(); // add the parent
    const promises = Object.keys(toPin).map(objectKey => 
      this.pinWithName(objectKey, [toPin[objectKey]])) // pin 'em.
        .then(() => objectKey) // return the object key when the promise is done
    );
    return accumulator.concat(promises);
  }, []);

  const fromPinPromise = this.fromPinWithName(pinName);
  const [fromPins, toPins] = await Promise.all([fromPinPromise, toPinPromises]);
  const allToPin = [...new Set([...(fromPins || []), ...toPins])];
  return this.pinWithName(pinName, allToPin);
},

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that but for some reason to get the toPins I would have to call Promise.all twice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have to be able to step through this with a debugger to see.

One thing I'm curious about is why this.pinWithName(objectKey, [toPin[objectKey]]))

I'm assuming that toPin[objectKey] is already an array, so not sure why it needs to be put in an array.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getChildren returns key -> object and not key -> [object]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having another pair of eyes debugging this is really helpful


// Removes object and children keys from pin name
// Keeps the object and children pinned
async _handleUnPinWithName(name: string, object: ParseObject) {
async _handleUnPinAllWithName(name: string, objects: Array<ParseObject>) {
const localDatastore = await this._getAllContents();
const pinName = this.getPinName(name);
const objects = this._getChildren(object);
const objectIds = Object.keys(objects);
objectIds.push(this.getKeyForObject(object));
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 => !objectIds.includes(item));
pinned = pinned.filter(item => !objectKeys.includes(item));
if (pinned.length == 0) {
await this.unPinWithName(pinName);
promises.push(this.unPinWithName(pinName));
delete localDatastore[pinName];
} else {
await this.pinWithName(pinName, pinned);
promises.push(this.pinWithName(pinName, pinned));
localDatastore[pinName] = pinned;
}
for (const objectKey of objectIds) {
for (const objectKey of objectKeys) {
let hasReference = false;
for (const key in localDatastore) {
if (key === DEFAULT_PIN || key.startsWith(PIN_PREFIX)) {
Expand All @@ -87,9 +123,10 @@ const LocalDatastore = {
}
}
if (!hasReference) {
await this.unPinWithName(objectKey);
promises.push(this.unPinWithName(objectKey));
}
}
return Promise.all(promises);
},

// Retrieve all pointer fields from object recursively
Expand Down Expand Up @@ -130,20 +167,22 @@ const LocalDatastore = {
const localDatastore = await this._getAllContents();
const allObjects = [];
for (const key in localDatastore) {
if (key !== DEFAULT_PIN && !key.startsWith(PIN_PREFIX)) {
allObjects.push(localDatastore[key]);
if (key.startsWith(OBJECT_PREFIX)) {
allObjects.push(localDatastore[key][0]);
}
}
if (!name) {
return Promise.resolve(allObjects);
return allObjects;
}
const pinName = await this.getPinName(name);
const pinned = await this.fromPinWithName(pinName);
const pinName = this.getPinName(name);
const pinned = localDatastore[pinName];
if (!Array.isArray(pinned)) {
return Promise.resolve([]);
return [];
}
const objects = pinned.map(async (objectKey) => await this.fromPinWithName(objectKey));
return Promise.all(objects);
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
Expand All @@ -154,10 +193,10 @@ const LocalDatastore = {
if (!LDS) {
LDS = await this._getAllContents();
}
const root = LDS[objectKey];
if (!root) {
if (!LDS[objectKey] || LDS[objectKey].length === 0) {
return null;
}
const root = LDS[objectKey][0];

const queue = [];
const meta = {};
Expand All @@ -172,8 +211,8 @@ const LocalDatastore = {
const value = subTreeRoot[field];
if (value.__type && value.__type === 'Object') {
const key = this.getKeyForObject(value);
const pointer = LDS[key];
if (pointer) {
if (LDS[key] && LDS[key].length > 0) {
const pointer = LDS[key][0];
uniqueId++;
meta[uniqueId] = pointer;
subTreeRoot[field] = pointer;
Expand All @@ -187,15 +226,16 @@ const LocalDatastore = {

// Called when an object is save / fetched
// Update object pin value
async _updateObjectIfPinned(object: ParseObject) {
async _updateObjectIfPinned(object: ParseObject): Promise {
if (!this.isEnabled) {
return;
}
const objectKey = this.getKeyForObject(object);
const pinned = await this.fromPinWithName(objectKey);
if (pinned) {
await this.pinWithName(objectKey, object._toFullJSON());
if (!pinned || pinned.length === 0) {
return;
}
return this.pinWithName(objectKey, [object._toFullJSON()]);
},

// Called when object is destroyed
Expand All @@ -211,7 +251,9 @@ const LocalDatastore = {
if (!pin) {
return;
}
await this.unPinWithName(objectKey);
const promises = [
this.unPinWithName(objectKey)
];
delete localDatastore[objectKey];

for (const key in localDatastore) {
Expand All @@ -220,31 +262,34 @@ const LocalDatastore = {
if (pinned.includes(objectKey)) {
pinned = pinned.filter(item => item !== objectKey);
if (pinned.length == 0) {
await this.unPinWithName(key);
promises.push(this.unPinWithName(key));
delete localDatastore[key];
} else {
await this.pinWithName(key, pinned);
promises.push(this.pinWithName(key, pinned));
localDatastore[key] = pinned;
}
}
}
}
return Promise.all(promises);
},

// Update pin and references of the unsaved object
async _updateLocalIdForObject(localId, object: ParseObject) {
if (!this.isEnabled) {
return;
}
const localKey = `${object.className}_${localId}`;
const localKey = `${OBJECT_PREFIX}${object.className}_${localId}`;
const objectKey = this.getKeyForObject(object);

const unsaved = await this.fromPinWithName(localKey);
if (!unsaved) {
if (!unsaved || unsaved.length === 0) {
return;
}
await this.unPinWithName(localKey);
await this.pinWithName(objectKey, unsaved);
const promises = [
this.unPinWithName(localKey),
this.pinWithName(objectKey, unsaved),
];

const localDatastore = await this._getAllContents();
for (const key in localDatastore) {
Expand All @@ -253,11 +298,12 @@ const LocalDatastore = {
if (pinned.includes(localKey)) {
pinned = pinned.filter(item => item !== localKey);
pinned.push(objectKey);
await this.pinWithName(key, pinned);
promises.push(this.pinWithName(key, pinned));
localDatastore[key] = pinned;
}
}
}
return Promise.all(promises);
},

/**
Expand All @@ -266,7 +312,8 @@ const LocalDatastore = {
* <pre>
* await Parse.LocalDatastore.updateFromServer();
* </pre>
*
* @method updateFromServer
* @name Parse.LocalDatastore.updateFromServer
* @static
*/
async updateFromServer() {
Expand All @@ -276,7 +323,7 @@ const LocalDatastore = {
const localDatastore = await this._getAllContents();
const keys = [];
for (const key in localDatastore) {
if (key !== DEFAULT_PIN && !key.startsWith(PIN_PREFIX)) {
if (key.startsWith(OBJECT_PREFIX)) {
keys.push(key);
}
}
Expand All @@ -286,7 +333,8 @@ const LocalDatastore = {
this.isSyncing = true;
const pointersHash = {};
for (const key of keys) {
const [className, objectId] = key.split('_');
// Ignore the OBJECT_PREFIX
const [ , , className, objectId] = key.split('_');
if (!(className in pointersHash)) {
pointersHash[className] = new Set();
}
Expand All @@ -313,15 +361,14 @@ const LocalDatastore = {
await Promise.all(pinPromises);
this.isSyncing = false;
} catch(error) {
console.log('Error syncing LocalDatastore'); // eslint-disable-line
console.log(error); // eslint-disable-line
console.error('Error syncing LocalDatastore: ', error);
this.isSyncing = false;
}
},

getKeyForObject(object: any) {
const objectId = object.objectId || object._getId();
return `${object.className}_${objectId}`;
return `${OBJECT_PREFIX}${object.className}_${objectId}`;
},

getPinName(pinName: ?string) {
Expand All @@ -333,14 +380,12 @@ const LocalDatastore = {

checkIfEnabled() {
if (!this.isEnabled) {
console.log('Parse.enableLocalDatastore() must be called first'); // eslint-disable-line no-console
console.error('Parse.enableLocalDatastore() must be called first');
}
return this.isEnabled;
}
};

LocalDatastore.DEFAULT_PIN = DEFAULT_PIN;
LocalDatastore.PIN_PREFIX = PIN_PREFIX;
LocalDatastore.isEnabled = false;
LocalDatastore.isSyncing = false;

Expand Down
Loading