Skip to content

Commit b6bc90c

Browse files
authored
LocalDatastore fixes for React-Native (#753)
* cleaner tests and docs * batch requests * getRawStorage * improve coverage * nits * nits * _handlePinWithName -> _handlePinAllWithName * _handleUnPinWithName -> _handleUnPinAllWithName * docs fix * optimizations * remove unused async * consistency with storing array values in LDS * fix logic * quick fix
1 parent 089e208 commit b6bc90c

16 files changed

+884
-551
lines changed

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"prefer-const": "error",
2424
"space-infix-ops": "error",
2525
"no-useless-escape": "off",
26-
"no-var": "error"
26+
"no-var": "error",
27+
"no-console": 0
2728
}
2829
}

integration/test/ParseLocalDatastoreTest.js

Lines changed: 226 additions & 176 deletions
Large diffs are not rendered by default.

integration/test/mockRNStorage.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ const mockRNStorage = {
2727
cb(undefined, Object.keys(mockStorage));
2828
},
2929

30+
multiGet(keys, cb) {
31+
const objects = keys.map((key) => [key, mockStorage[key]]);
32+
cb(undefined, objects);
33+
},
34+
35+
multiRemove(keys, cb) {
36+
keys.map((key) => delete mockStorage[key]);
37+
cb(undefined);
38+
},
39+
3040
clear() {
3141
mockStorage = {};
3242
},

src/.flowconfig

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
[ignore]
2-
.*/node_modules/.*
2+
.*/node_modules/
3+
.*/lib/
34

45
[include]
5-
../package.json
66

77
[libs]
8-
interfaces/
98

109
[options]
11-
unsafe.enable_getters_and_setters=true
10+
suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next

src/LocalDatastore.js

Lines changed: 98 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,28 @@ import CoreManager from './CoreManager';
1313

1414
import type ParseObject from './ParseObject';
1515
import ParseQuery from './ParseQuery';
16+
import { DEFAULT_PIN, PIN_PREFIX, OBJECT_PREFIX } from './LocalDatastoreUtils';
1617

17-
const DEFAULT_PIN = '_default';
18-
const PIN_PREFIX = 'parsePin_';
19-
18+
/**
19+
* Provides a local datastore which can be used to store and retrieve <code>Parse.Object</code>. <br />
20+
* To enable this functionality, call <code>Parse.enableLocalDatastore()</code>.
21+
*
22+
* Pin object to add to local datastore
23+
*
24+
* <pre>await object.pin();</pre>
25+
* <pre>await object.pinWithName('pinName');</pre>
26+
*
27+
* Query pinned objects
28+
*
29+
* <pre>query.fromLocalDatastore();</pre>
30+
* <pre>query.fromPin();</pre>
31+
* <pre>query.fromPinWithName();</pre>
32+
*
33+
* <pre>const localObjects = await query.find();</pre>
34+
*
35+
* @class Parse.LocalDatastore
36+
* @static
37+
*/
2038
const LocalDatastore = {
2139
fromPinWithName(name: string): Promise {
2240
const controller = CoreManager.getLocalDatastoreController();
@@ -38,44 +56,62 @@ const LocalDatastore = {
3856
return controller.getAllContents();
3957
},
4058

59+
// Use for testing
60+
_getRawStorage(): Promise {
61+
const controller = CoreManager.getLocalDatastoreController();
62+
return controller.getRawStorage();
63+
},
64+
4165
_clear(): Promise {
4266
const controller = CoreManager.getLocalDatastoreController();
4367
return controller.clear();
4468
},
4569

4670
// Pin the object and children recursively
4771
// Saves the object and children key to Pin Name
48-
async _handlePinWithName(name: string, object: ParseObject): Promise {
72+
async _handlePinAllWithName(name: string, objects: Array<ParseObject>): Promise {
4973
const pinName = this.getPinName(name);
50-
const objects = this._getChildren(object);
51-
objects[this.getKeyForObject(object)] = object._toFullJSON();
52-
for (const objectKey in objects) {
53-
await this.pinWithName(objectKey, objects[objectKey]);
74+
const toPinPromises = [];
75+
const objectKeys = [];
76+
for (const parent of objects) {
77+
const children = this._getChildren(parent);
78+
const parentKey = this.getKeyForObject(parent);
79+
children[parentKey] = parent._toFullJSON();
80+
for (const objectKey in children) {
81+
objectKeys.push(objectKey);
82+
toPinPromises.push(this.pinWithName(objectKey, [children[objectKey]]));
83+
}
5484
}
55-
const pinned = await this.fromPinWithName(pinName) || [];
56-
const objectIds = Object.keys(objects);
57-
const toPin = [...new Set([...pinned, ...objectIds])];
58-
await this.pinWithName(pinName, toPin);
85+
const fromPinPromise = this.fromPinWithName(pinName);
86+
const [pinned] = await Promise.all([fromPinPromise, toPinPromises]);
87+
const toPin = [...new Set([...(pinned || []), ...objectKeys])];
88+
return this.pinWithName(pinName, toPin);
5989
},
6090

6191
// Removes object and children keys from pin name
6292
// Keeps the object and children pinned
63-
async _handleUnPinWithName(name: string, object: ParseObject) {
93+
async _handleUnPinAllWithName(name: string, objects: Array<ParseObject>) {
6494
const localDatastore = await this._getAllContents();
6595
const pinName = this.getPinName(name);
66-
const objects = this._getChildren(object);
67-
const objectIds = Object.keys(objects);
68-
objectIds.push(this.getKeyForObject(object));
96+
const promises = [];
97+
let objectKeys = [];
98+
for (const parent of objects) {
99+
const children = this._getChildren(parent);
100+
const parentKey = this.getKeyForObject(parent);
101+
objectKeys.push(parentKey, ...Object.keys(children));
102+
}
103+
objectKeys = [...new Set(objectKeys)];
104+
69105
let pinned = localDatastore[pinName] || [];
70-
pinned = pinned.filter(item => !objectIds.includes(item));
106+
pinned = pinned.filter(item => !objectKeys.includes(item));
71107
if (pinned.length == 0) {
72-
await this.unPinWithName(pinName);
108+
promises.push(this.unPinWithName(pinName));
73109
delete localDatastore[pinName];
74110
} else {
75-
await this.pinWithName(pinName, pinned);
111+
promises.push(this.pinWithName(pinName, pinned));
76112
localDatastore[pinName] = pinned;
77113
}
78-
for (const objectKey of objectIds) {
114+
for (const objectKey of objectKeys) {
79115
let hasReference = false;
80116
for (const key in localDatastore) {
81117
if (key === DEFAULT_PIN || key.startsWith(PIN_PREFIX)) {
@@ -87,9 +123,10 @@ const LocalDatastore = {
87123
}
88124
}
89125
if (!hasReference) {
90-
await this.unPinWithName(objectKey);
126+
promises.push(this.unPinWithName(objectKey));
91127
}
92128
}
129+
return Promise.all(promises);
93130
},
94131

95132
// Retrieve all pointer fields from object recursively
@@ -130,20 +167,22 @@ const LocalDatastore = {
130167
const localDatastore = await this._getAllContents();
131168
const allObjects = [];
132169
for (const key in localDatastore) {
133-
if (key !== DEFAULT_PIN && !key.startsWith(PIN_PREFIX)) {
134-
allObjects.push(localDatastore[key]);
170+
if (key.startsWith(OBJECT_PREFIX)) {
171+
allObjects.push(localDatastore[key][0]);
135172
}
136173
}
137174
if (!name) {
138-
return Promise.resolve(allObjects);
175+
return allObjects;
139176
}
140-
const pinName = await this.getPinName(name);
141-
const pinned = await this.fromPinWithName(pinName);
177+
const pinName = this.getPinName(name);
178+
const pinned = localDatastore[pinName];
142179
if (!Array.isArray(pinned)) {
143-
return Promise.resolve([]);
180+
return [];
144181
}
145-
const objects = pinned.map(async (objectKey) => await this.fromPinWithName(objectKey));
146-
return Promise.all(objects);
182+
const promises = pinned.map((objectKey) => this.fromPinWithName(objectKey));
183+
let objects = await Promise.all(promises);
184+
objects = [].concat(...objects);
185+
return objects.filter(object => object != null);
147186
},
148187

149188
// Replaces object pointers with pinned pointers
@@ -154,10 +193,10 @@ const LocalDatastore = {
154193
if (!LDS) {
155194
LDS = await this._getAllContents();
156195
}
157-
const root = LDS[objectKey];
158-
if (!root) {
196+
if (!LDS[objectKey] || LDS[objectKey].length === 0) {
159197
return null;
160198
}
199+
const root = LDS[objectKey][0];
161200

162201
const queue = [];
163202
const meta = {};
@@ -172,8 +211,8 @@ const LocalDatastore = {
172211
const value = subTreeRoot[field];
173212
if (value.__type && value.__type === 'Object') {
174213
const key = this.getKeyForObject(value);
175-
const pointer = LDS[key];
176-
if (pointer) {
214+
if (LDS[key] && LDS[key].length > 0) {
215+
const pointer = LDS[key][0];
177216
uniqueId++;
178217
meta[uniqueId] = pointer;
179218
subTreeRoot[field] = pointer;
@@ -187,15 +226,16 @@ const LocalDatastore = {
187226

188227
// Called when an object is save / fetched
189228
// Update object pin value
190-
async _updateObjectIfPinned(object: ParseObject) {
229+
async _updateObjectIfPinned(object: ParseObject): Promise {
191230
if (!this.isEnabled) {
192231
return;
193232
}
194233
const objectKey = this.getKeyForObject(object);
195234
const pinned = await this.fromPinWithName(objectKey);
196-
if (pinned) {
197-
await this.pinWithName(objectKey, object._toFullJSON());
235+
if (!pinned || pinned.length === 0) {
236+
return;
198237
}
238+
return this.pinWithName(objectKey, [object._toFullJSON()]);
199239
},
200240

201241
// Called when object is destroyed
@@ -211,7 +251,9 @@ const LocalDatastore = {
211251
if (!pin) {
212252
return;
213253
}
214-
await this.unPinWithName(objectKey);
254+
const promises = [
255+
this.unPinWithName(objectKey)
256+
];
215257
delete localDatastore[objectKey];
216258

217259
for (const key in localDatastore) {
@@ -220,31 +262,34 @@ const LocalDatastore = {
220262
if (pinned.includes(objectKey)) {
221263
pinned = pinned.filter(item => item !== objectKey);
222264
if (pinned.length == 0) {
223-
await this.unPinWithName(key);
265+
promises.push(this.unPinWithName(key));
224266
delete localDatastore[key];
225267
} else {
226-
await this.pinWithName(key, pinned);
268+
promises.push(this.pinWithName(key, pinned));
227269
localDatastore[key] = pinned;
228270
}
229271
}
230272
}
231273
}
274+
return Promise.all(promises);
232275
},
233276

234277
// Update pin and references of the unsaved object
235278
async _updateLocalIdForObject(localId, object: ParseObject) {
236279
if (!this.isEnabled) {
237280
return;
238281
}
239-
const localKey = `${object.className}_${localId}`;
282+
const localKey = `${OBJECT_PREFIX}${object.className}_${localId}`;
240283
const objectKey = this.getKeyForObject(object);
241284

242285
const unsaved = await this.fromPinWithName(localKey);
243-
if (!unsaved) {
286+
if (!unsaved || unsaved.length === 0) {
244287
return;
245288
}
246-
await this.unPinWithName(localKey);
247-
await this.pinWithName(objectKey, unsaved);
289+
const promises = [
290+
this.unPinWithName(localKey),
291+
this.pinWithName(objectKey, unsaved),
292+
];
248293

249294
const localDatastore = await this._getAllContents();
250295
for (const key in localDatastore) {
@@ -253,11 +298,12 @@ const LocalDatastore = {
253298
if (pinned.includes(localKey)) {
254299
pinned = pinned.filter(item => item !== localKey);
255300
pinned.push(objectKey);
256-
await this.pinWithName(key, pinned);
301+
promises.push(this.pinWithName(key, pinned));
257302
localDatastore[key] = pinned;
258303
}
259304
}
260305
}
306+
return Promise.all(promises);
261307
},
262308

263309
/**
@@ -266,7 +312,8 @@ const LocalDatastore = {
266312
* <pre>
267313
* await Parse.LocalDatastore.updateFromServer();
268314
* </pre>
269-
*
315+
* @method updateFromServer
316+
* @name Parse.LocalDatastore.updateFromServer
270317
* @static
271318
*/
272319
async updateFromServer() {
@@ -276,7 +323,7 @@ const LocalDatastore = {
276323
const localDatastore = await this._getAllContents();
277324
const keys = [];
278325
for (const key in localDatastore) {
279-
if (key !== DEFAULT_PIN && !key.startsWith(PIN_PREFIX)) {
326+
if (key.startsWith(OBJECT_PREFIX)) {
280327
keys.push(key);
281328
}
282329
}
@@ -286,7 +333,8 @@ const LocalDatastore = {
286333
this.isSyncing = true;
287334
const pointersHash = {};
288335
for (const key of keys) {
289-
const [className, objectId] = key.split('_');
336+
// Ignore the OBJECT_PREFIX
337+
const [ , , className, objectId] = key.split('_');
290338
if (!(className in pointersHash)) {
291339
pointersHash[className] = new Set();
292340
}
@@ -313,15 +361,14 @@ const LocalDatastore = {
313361
await Promise.all(pinPromises);
314362
this.isSyncing = false;
315363
} catch(error) {
316-
console.log('Error syncing LocalDatastore'); // eslint-disable-line
317-
console.log(error); // eslint-disable-line
364+
console.error('Error syncing LocalDatastore: ', error);
318365
this.isSyncing = false;
319366
}
320367
},
321368

322369
getKeyForObject(object: any) {
323370
const objectId = object.objectId || object._getId();
324-
return `${object.className}_${objectId}`;
371+
return `${OBJECT_PREFIX}${object.className}_${objectId}`;
325372
},
326373

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

334381
checkIfEnabled() {
335382
if (!this.isEnabled) {
336-
console.log('Parse.enableLocalDatastore() must be called first'); // eslint-disable-line no-console
383+
console.error('Parse.enableLocalDatastore() must be called first');
337384
}
338385
return this.isEnabled;
339386
}
340387
};
341388

342-
LocalDatastore.DEFAULT_PIN = DEFAULT_PIN;
343-
LocalDatastore.PIN_PREFIX = PIN_PREFIX;
344389
LocalDatastore.isEnabled = false;
345390
LocalDatastore.isSyncing = false;
346391

0 commit comments

Comments
 (0)