Skip to content
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

LocalDatastore fixes for React-Native #753

Merged
merged 18 commits into from
Mar 26, 2019
Merged
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
60 changes: 43 additions & 17 deletions src/LocalDatastore.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,30 @@ 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>.
*
* To store objects you can pin them.
dplewis marked this conversation as resolved.
Show resolved Hide resolved
*
* <pre>await object.pin();</pre>
* <pre>await object.pinWithName('pinName');</pre>
*
* You can query your objects by changing the source of your query.
*
dplewis marked this conversation as resolved.
Show resolved Hide resolved
* <pre>query.fromLocalDatastore();</pre>
* <pre>query.fromPin();</pre>
* <pre>query.fromPinWithName();</pre>
*
* Once your source is changed, you can query your objects from LocalDatastore
dplewis marked this conversation as resolved.
Show resolved Hide resolved
*
* <pre>const localObjects = await query.find();</pre>
*
* @class Parse.LocalDatastore
* @static
*/
const LocalDatastore = {
fromPinWithName(name: string): Promise {
const controller = CoreManager.getLocalDatastoreController();
Expand All @@ -38,6 +58,12 @@ 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();
Expand Down Expand Up @@ -130,7 +156,7 @@ const LocalDatastore = {
const localDatastore = await this._getAllContents();
const allObjects = [];
for (const key in localDatastore) {
if (key !== DEFAULT_PIN && !key.startsWith(PIN_PREFIX)) {
if (key.startsWith(OBJECT_PREFIX)) {
allObjects.push(localDatastore[key]);
}
}
Expand Down Expand Up @@ -187,15 +213,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;
return Promise.resolve();
}
const objectKey = this.getKeyForObject(object);
const pinned = await this.fromPinWithName(objectKey);
if (pinned) {
await this.pinWithName(objectKey, object._toFullJSON());
if (!pinned) {
return Promise.resolve();
}
return this.pinWithName(objectKey, object._toFullJSON());
},

// Called when object is destroyed
Expand Down Expand Up @@ -236,7 +263,7 @@ const LocalDatastore = {
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);
Expand Down Expand Up @@ -266,7 +293,8 @@ const LocalDatastore = {
* <pre>
* await Parse.LocalDatastore.updateFromServer();
* </pre>
*
* @method updateFromServer
* @name Parse.LocalDatastore.updateFromServer
* @static
*/
async updateFromServer() {
Expand All @@ -276,7 +304,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 +314,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 +342,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.log('Error syncing LocalDatastore: ', error); // eslint-disable-line
dplewis marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -339,8 +367,6 @@ const LocalDatastore = {
}
};

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

Expand Down
29 changes: 26 additions & 3 deletions src/LocalDatastoreController.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

/* global localStorage */
import { isLocalDatastoreKey } from './LocalDatastoreUtils';

const LocalDatastoreController = {
fromPinWithName(name: string): Promise {
Expand Down Expand Up @@ -40,14 +41,36 @@ const LocalDatastoreController = {
const LDS = {};
for (let i = 0; i < localStorage.length; i += 1) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
LDS[key] = JSON.parse(value);
if (isLocalDatastoreKey(key)) {
const value = localStorage.getItem(key);
LDS[key] = JSON.parse(value);
}
}
return Promise.resolve(LDS);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
},

getRawStorage(): Promise {
const storage = {};
for (let i = 0; i < localStorage.length; i += 1) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
storage[key] = value;
}
return Promise.resolve(storage);
},

clear(): Promise {
return Promise.resolve(localStorage.clear());
const toRemove = [];
for (let i = 0; i < localStorage.length; i += 1) {
const key = localStorage.key(i);
if (isLocalDatastoreKey(key)) {
toRemove.push(key);
}
}
for (const key of toRemove) {
localStorage.removeItem(key);
}
return Promise.resolve();
}
};

Expand Down
38 changes: 27 additions & 11 deletions src/LocalDatastoreController.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,50 @@
*
* @flow
*/
import { isLocalDatastoreKey } from './LocalDatastoreUtils';

const memMap = {};
const LocalDatastoreController = {
fromPinWithName(name: string): ?any {
if (memMap.hasOwnProperty(name)) {
return memMap[name];
fromPinWithName(name: string): Promise {
dplewis marked this conversation as resolved.
Show resolved Hide resolved
if (!memMap.hasOwnProperty(name)) {
return Promise.resolve(null);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
}
return null;
const objects = JSON.parse(memMap[name]);
return Promise.resolve(objects);
},

pinWithName(name: string, value: any) {
memMap[name] = value;
pinWithName(name: string, value: any): Promise {
dplewis marked this conversation as resolved.
Show resolved Hide resolved
const values = JSON.stringify(value);
memMap[name] = values;
return Promise.resolve();
},

unPinWithName(name: string) {
unPinWithName(name: string): Promise {
delete memMap[name];
return Promise.resolve();
},

getAllContents() {
return memMap;
getAllContents(): Promise {
const LDS = {};
for (const key in memMap) {
if (memMap.hasOwnProperty(key) && isLocalDatastoreKey(key)) {
LDS[key] = JSON.parse(memMap[key]);
}
}
return Promise.resolve(LDS);
},

getRawStorage(): Promise {
return Promise.resolve(memMap);
},

clear() {
clear(): Promise {
dplewis marked this conversation as resolved.
Show resolved Hide resolved
for (const key in memMap) {
if (memMap.hasOwnProperty(key)) {
if (memMap.hasOwnProperty(key) && isLocalDatastoreKey(key)) {
delete memMap[key];
}
}
return Promise.resolve();
}
};

Expand Down
45 changes: 40 additions & 5 deletions src/LocalDatastoreController.react-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

const RNStorage = require('./StorageController.react-native');
import { isLocalDatastoreKey } from './LocalDatastoreUtils';

const LocalDatastoreController = {
async fromPinWithName(name: string): Promise {
Expand Down Expand Up @@ -38,18 +39,52 @@ const LocalDatastoreController = {
},

async getAllContents(): Promise {
const LDS = {};
const keys = await RNStorage.getAllKeys();
const batch = [];
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const value = await RNStorage.getItemAsync(key);
LDS[key] = JSON.parse(value);
if (isLocalDatastoreKey(key)) {
batch.push(key);
}
}
const LDS = {};
const results = await RNStorage.multiGet(batch);
results.map((pair) => {
const [key, value] = pair;
try {
LDS[key] = JSON.parse(value);
} catch (error) {
LDS[key] = null;
}
});
return Promise.resolve(LDS);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
},

clear(): Promise {
return RNStorage.clear();
async getRawStorage(): Promise {
dplewis marked this conversation as resolved.
Show resolved Hide resolved
const keys = await RNStorage.getAllKeys();
const storage = {};
const results = await RNStorage.multiGet(keys);
results.map((pair) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

can use forEach here instead of map cause you're not capturing the returned array.

map returns an array with one element per element in the original array
forEach return nothing.

so forEach is more memory efficient if you're not returning something.

map composes very nicely with filter.

const [key, value] = pair;
storage[key] = value;
});
return Promise.resolve(storage);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
},

async clear(): void {
try {
const keys = await RNStorage.getAllKeys();
const batch = [];
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (isLocalDatastoreKey(key)) {
batch.push(key);
}
}
await RNStorage.multiRemove(batch);
dplewis marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
console.log(error); // eslint-disable-line
}
}
};

Expand Down
24 changes: 24 additions & 0 deletions src/LocalDatastoreUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
const DEFAULT_PIN = '_default';
const PIN_PREFIX = 'parsePin_';
const OBJECT_PREFIX = 'Parse_LDS_';

function isLocalDatastoreKey(key: string): boolean {
return !!(key && (key === DEFAULT_PIN || key.startsWith(PIN_PREFIX) || key.startsWith(OBJECT_PREFIX)));
}

export {
DEFAULT_PIN,
PIN_PREFIX,
OBJECT_PREFIX,
isLocalDatastoreKey,
};
Loading