Skip to content

Commit 945359f

Browse files
authored
LocalDatastore can update from server (#734)
* lds sync * clean-up * rename sync to update * clean up
1 parent 41010b9 commit 945359f

File tree

4 files changed

+232
-2
lines changed

4 files changed

+232
-2
lines changed

integration/test/ParseLocalDatastoreTest.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,29 @@ function runTest(controller) {
802802
assert.equal(localDatastore[`Item_${fetchedItems[0].id}`].foo, 'changed');
803803
assert.equal(localDatastore[`Item_${fetchedItems[1].id}`].foo, 'changed');
804804
});
805+
806+
it(`${controller.name} can update Local Datastore from network`, async () => {
807+
const parent = new TestObject();
808+
const child = new Item();
809+
const grandchild = new Item();
810+
child.set('grandchild', grandchild);
811+
parent.set('field', 'test');
812+
parent.set('child', child);
813+
await Parse.Object.saveAll([parent, child, grandchild]);
814+
await parent.pin();
815+
816+
// Updates child with { foo: 'changed' }
817+
const params = { id: child.id };
818+
await Parse.Cloud.run('TestFetchFromLocalDatastore', params);
819+
820+
Parse.LocalDatastore.isSyncing = false;
821+
822+
await Parse.LocalDatastore.updateFromServer();
823+
824+
const updatedLDS = await Parse.LocalDatastore._getAllContents();
825+
const childJSON = updatedLDS[`${child.className}_${child.id}`];
826+
assert.equal(childJSON.foo, 'changed');
827+
});
805828
});
806829

807830
describe(`Parse Query Pinning (${controller.name})`, () => {
@@ -2344,7 +2367,7 @@ function runTest(controller) {
23442367
});
23452368
});
23462369

2347-
it('supports withinPolygon', async () => {
2370+
it(`${controller.name} supports withinPolygon`, async () => {
23482371
const sacramento = new TestObject();
23492372
sacramento.set('location', new Parse.GeoPoint(38.52, -121.50));
23502373
sacramento.set('name', 'Sacramento');
@@ -2373,7 +2396,7 @@ function runTest(controller) {
23732396
assert.equal(results.length, 1);
23742397
});
23752398

2376-
it('supports polygonContains', async () => {
2399+
it(`${controller.name} supports polygonContains`, async () => {
23772400
const p1 = [[0,0], [0,1], [1,1], [1,0]];
23782401
const p2 = [[0,0], [0,2], [2,2], [2,0]];
23792402
const p3 = [[10,10], [10,15], [15,15], [15,10], [10,10]];

src/LocalDatastore.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import CoreManager from './CoreManager';
1313

1414
import type ParseObject from './ParseObject';
15+
import ParseQuery from './ParseQuery';
1516

1617
const DEFAULT_PIN = '_default';
1718
const PIN_PREFIX = 'parsePin_';
@@ -259,6 +260,65 @@ const LocalDatastore = {
259260
}
260261
},
261262

263+
/**
264+
* Updates Local Datastore from Server
265+
*
266+
* <pre>
267+
* await Parse.LocalDatastore.updateFromServer();
268+
* </pre>
269+
*
270+
* @static
271+
*/
272+
async updateFromServer() {
273+
if (!this.checkIfEnabled() || this.isSyncing) {
274+
return;
275+
}
276+
const localDatastore = await this._getAllContents();
277+
const keys = [];
278+
for (const key in localDatastore) {
279+
if (key !== DEFAULT_PIN && !key.startsWith(PIN_PREFIX)) {
280+
keys.push(key);
281+
}
282+
}
283+
if (keys.length === 0) {
284+
return;
285+
}
286+
this.isSyncing = true;
287+
const pointersHash = {};
288+
for (const key of keys) {
289+
const [className, objectId] = key.split('_');
290+
if (!(className in pointersHash)) {
291+
pointersHash[className] = new Set();
292+
}
293+
pointersHash[className].add(objectId);
294+
}
295+
const queryPromises = Object.keys(pointersHash).map(className => {
296+
const objectIds = Array.from(pointersHash[className]);
297+
const query = new ParseQuery(className);
298+
query.limit(objectIds.length);
299+
if (objectIds.length === 1) {
300+
query.equalTo('objectId', objectIds[0]);
301+
} else {
302+
query.containedIn('objectId', objectIds);
303+
}
304+
return query.find();
305+
});
306+
try {
307+
const responses = await Promise.all(queryPromises);
308+
const objects = [].concat.apply([], responses);
309+
const pinPromises = objects.map((object) => {
310+
const objectKey = this.getKeyForObject(object);
311+
return this.pinWithName(objectKey, object._toFullJSON());
312+
});
313+
await Promise.all(pinPromises);
314+
this.isSyncing = false;
315+
} catch(error) {
316+
console.log('Error syncing LocalDatastore'); // eslint-disable-line
317+
console.log(error); // eslint-disable-line
318+
this.isSyncing = false;
319+
}
320+
},
321+
262322
getKeyForObject(object: any) {
263323
const objectId = object.objectId || object._getId();
264324
return `${object.className}_${objectId}`;
@@ -282,6 +342,7 @@ const LocalDatastore = {
282342
LocalDatastore.DEFAULT_PIN = DEFAULT_PIN;
283343
LocalDatastore.PIN_PREFIX = PIN_PREFIX;
284344
LocalDatastore.isEnabled = false;
345+
LocalDatastore.isSyncing = false;
285346

286347
module.exports = LocalDatastore;
287348

src/__tests__/LocalDatastore-test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,21 @@ const mockLocalStorageController = {
8282
clear: jest.fn(),
8383
};
8484
jest.setMock('../ParseObject', MockObject);
85+
86+
const mockQueryFind = jest.fn();
87+
jest.mock('../ParseQuery', () => {
88+
return jest.fn().mockImplementation(function () {
89+
this.equalTo = jest.fn();
90+
this.containedIn = jest.fn();
91+
this.limit = jest.fn();
92+
this.find = mockQueryFind;
93+
});
94+
});
95+
8596
const CoreManager = require('../CoreManager');
8697
const LocalDatastore = require('../LocalDatastore');
8798
const ParseObject = require('../ParseObject');
99+
const ParseQuery = require('../ParseQuery');
88100
const RNDatastoreController = require('../LocalDatastoreController.react-native');
89101
const BrowserDatastoreController = require('../LocalDatastoreController.browser');
90102
const DefaultDatastoreController = require('../LocalDatastoreController.default');
@@ -572,6 +584,139 @@ describe('LocalDatastore', () => {
572584
LocalDatastore._traverse(object, encountered);
573585
expect(encountered).toEqual({ 'Item_1234': object });
574586
});
587+
588+
it('do not sync if disabled', async () => {
589+
LocalDatastore.isEnabled = false;
590+
jest.spyOn(mockLocalStorageController, 'getAllContents');
591+
592+
await LocalDatastore.updateFromServer();
593+
expect(LocalDatastore.isSyncing).toBe(false);
594+
expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(0);
595+
});
596+
597+
it('do not sync if syncing', async () => {
598+
LocalDatastore.isEnabled = true;
599+
LocalDatastore.isSyncing = true;
600+
601+
jest.spyOn(mockLocalStorageController, 'getAllContents');
602+
await LocalDatastore.updateFromServer();
603+
604+
expect(LocalDatastore.isSyncing).toBe(true);
605+
expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(0);
606+
});
607+
608+
it('updateFromServer empty LDS', async () => {
609+
LocalDatastore.isEnabled = true;
610+
LocalDatastore.isSyncing = false;
611+
const LDS = {};
612+
613+
mockLocalStorageController
614+
.getAllContents
615+
.mockImplementationOnce(() => LDS);
616+
617+
jest.spyOn(mockLocalStorageController, 'pinWithName');
618+
await LocalDatastore.updateFromServer();
619+
620+
expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(0);
621+
});
622+
623+
it('updateFromServer on one object', async () => {
624+
LocalDatastore.isEnabled = true;
625+
LocalDatastore.isSyncing = false;
626+
const object = new ParseObject('Item');
627+
const LDS = {
628+
[`Item_${object.id}`]: object._toFullJSON(),
629+
[`${LocalDatastore.PIN_PREFIX}_testPinName`]: [`Item_${object.id}`],
630+
[LocalDatastore.DEFAULT_PIN]: [`Item_${object.id}`],
631+
};
632+
633+
mockLocalStorageController
634+
.getAllContents
635+
.mockImplementationOnce(() => LDS);
636+
637+
object.set('updatedField', 'foo');
638+
mockQueryFind.mockImplementationOnce(() => Promise.resolve([object]));
639+
640+
await LocalDatastore.updateFromServer();
641+
642+
expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1);
643+
expect(ParseQuery).toHaveBeenCalledTimes(1);
644+
const mockQueryInstance = ParseQuery.mock.instances[0];
645+
646+
expect(mockQueryInstance.equalTo.mock.calls.length).toBe(1);
647+
expect(mockQueryFind).toHaveBeenCalledTimes(1);
648+
expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(1);
649+
});
650+
651+
it('updateFromServer handle error', async () => {
652+
LocalDatastore.isEnabled = true;
653+
LocalDatastore.isSyncing = false;
654+
const object = new ParseObject('Item');
655+
const LDS = {
656+
[`Item_${object.id}`]: object._toFullJSON(),
657+
[`${LocalDatastore.PIN_PREFIX}_testPinName`]: [`Item_${object.id}`],
658+
[LocalDatastore.DEFAULT_PIN]: [`Item_${object.id}`],
659+
};
660+
661+
mockLocalStorageController
662+
.getAllContents
663+
.mockImplementationOnce(() => LDS);
664+
665+
object.set('updatedField', 'foo');
666+
mockQueryFind.mockImplementationOnce(() => {
667+
expect(LocalDatastore.isSyncing).toBe(true);
668+
return Promise.reject('Unable to connect to the Parse API')
669+
});
670+
671+
jest.spyOn(console, 'log');
672+
await LocalDatastore.updateFromServer();
673+
674+
expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1);
675+
expect(ParseQuery).toHaveBeenCalledTimes(1);
676+
const mockQueryInstance = ParseQuery.mock.instances[0];
677+
678+
expect(mockQueryInstance.equalTo.mock.calls.length).toBe(1);
679+
expect(mockQueryFind).toHaveBeenCalledTimes(1);
680+
expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(0);
681+
expect(console.log).toHaveBeenCalledTimes(2);
682+
expect(LocalDatastore.isSyncing).toBe(false);
683+
});
684+
685+
it('updateFromServer on mixed object', async () => {
686+
LocalDatastore.isEnabled = true;
687+
LocalDatastore.isSyncing = false;
688+
const obj1 = new ParseObject('Item');
689+
const obj2 = new ParseObject('Item');
690+
const obj3 = new ParseObject('TestObject');
691+
const LDS = {
692+
[`Item_${obj1.id}`]: obj1._toFullJSON(),
693+
[`Item_${obj2.id}`]: obj2._toFullJSON(),
694+
[`TestObject_${obj3.id}`]: obj3._toFullJSON(),
695+
[`${LocalDatastore.PIN_PREFIX}_testPinName`]: [`Item_${obj1.id}`],
696+
[LocalDatastore.DEFAULT_PIN]: [`Item_${obj1.id}`],
697+
};
698+
699+
mockLocalStorageController
700+
.getAllContents
701+
.mockImplementationOnce(() => LDS);
702+
703+
mockQueryFind
704+
.mockImplementationOnce(() => Promise.resolve([obj1, obj2]))
705+
.mockImplementationOnce(() => Promise.resolve([obj3]));
706+
707+
await LocalDatastore.updateFromServer();
708+
709+
expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1);
710+
expect(ParseQuery).toHaveBeenCalledTimes(2);
711+
712+
const mockQueryInstance1 = ParseQuery.mock.instances[0];
713+
const mockQueryInstance2 = ParseQuery.mock.instances[1];
714+
715+
expect(mockQueryInstance1.containedIn.mock.calls.length).toBe(1);
716+
expect(mockQueryInstance2.equalTo.mock.calls.length).toBe(1);
717+
expect(mockQueryFind).toHaveBeenCalledTimes(2);
718+
expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(3);
719+
});
575720
});
576721

577722
describe('BrowserDatastoreController', async () => {

src/__tests__/ParseObject-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const mockLocalDatastore = {
119119
_updateObjectIfPinned: jest.fn(),
120120
_destroyObjectIfPinned: jest.fn(),
121121
_updateLocalIdForObject: jest.fn(),
122+
updateFromServer: jest.fn(),
122123
_clear: jest.fn(),
123124
getKeyForObject: jest.fn(),
124125
checkIfEnabled: jest.fn(() => {

0 commit comments

Comments
 (0)