Skip to content

Commit 340bcf2

Browse files
committed
Add EventuallyQueue API
1 parent 0c84f19 commit 340bcf2

File tree

4 files changed

+683
-0
lines changed

4 files changed

+683
-0
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const clear = require('./clear');
5+
const Parse = require('../../node');
6+
const sleep = require('./sleep');
7+
8+
const TestObject = Parse.Object.extend('TestObject');
9+
10+
describe('Parse EventuallyQueue', () => {
11+
beforeEach(done => {
12+
Parse.initialize('integration', null, 'notsosecret');
13+
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
14+
Parse.Storage._clear();
15+
clear().then(() => {
16+
done();
17+
});
18+
});
19+
20+
it('can queue save object', async () => {
21+
const object = new TestObject({ test: 'test' });
22+
await object.save();
23+
object.set('foo', 'bar');
24+
await Parse.EventuallyQueue.save(object);
25+
await Parse.EventuallyQueue.sendQueue();
26+
27+
const query = new Parse.Query(TestObject);
28+
const result = await query.get(object.id);
29+
assert.strictEqual(result.get('foo'), 'bar');
30+
31+
const length = await Parse.EventuallyQueue.length();
32+
assert.strictEqual(length, 0);
33+
});
34+
35+
it('can queue destroy object', async () => {
36+
const object = new TestObject({ test: 'test' });
37+
await object.save();
38+
await Parse.EventuallyQueue.destroy(object);
39+
await Parse.EventuallyQueue.sendQueue();
40+
41+
const query = new Parse.Query(TestObject);
42+
query.equalTo('objectId', object.id);
43+
const results = await query.find();
44+
assert.strictEqual(results.length, 0);
45+
46+
const length = await Parse.EventuallyQueue.length();
47+
assert.strictEqual(length, 0);
48+
});
49+
50+
it('can queue multiple object', async () => {
51+
const obj1 = new TestObject({ foo: 'bar' });
52+
const obj2 = new TestObject({ foo: 'baz' });
53+
const obj3 = new TestObject({ foo: 'bag' });
54+
await Parse.EventuallyQueue.save(obj1);
55+
await Parse.EventuallyQueue.save(obj2);
56+
await Parse.EventuallyQueue.save(obj3);
57+
58+
let length = await Parse.EventuallyQueue.length();
59+
assert.strictEqual(length, 3);
60+
61+
await Parse.EventuallyQueue.sendQueue();
62+
63+
const query = new Parse.Query(TestObject);
64+
query.ascending('createdAt');
65+
const results = await query.find();
66+
assert.strictEqual(results.length, 3);
67+
assert.strictEqual(results[0].get('foo'), 'bar');
68+
assert.strictEqual(results[1].get('foo'), 'baz');
69+
assert.strictEqual(results[2].get('foo'), 'bag');
70+
71+
length = await Parse.EventuallyQueue.length();
72+
assert.strictEqual(length, 0);
73+
74+
// TODO: can't use obj1, etc because they don't have an id
75+
await Parse.EventuallyQueue.destroy(results[0]);
76+
await Parse.EventuallyQueue.destroy(results[1]);
77+
await Parse.EventuallyQueue.destroy(results[2]);
78+
79+
length = await Parse.EventuallyQueue.length();
80+
assert.strictEqual(length, 3);
81+
82+
await Parse.EventuallyQueue.sendQueue();
83+
const objects = await query.find();
84+
assert.strictEqual(objects.length, 0);
85+
});
86+
87+
it('can queue destroy for object that does not exist', async () => {
88+
const object = new TestObject({ test: 'test' });
89+
await object.save();
90+
await object.destroy();
91+
await Parse.EventuallyQueue.destroy(object);
92+
await Parse.EventuallyQueue.sendQueue();
93+
94+
const length = await Parse.EventuallyQueue.length();
95+
assert.strictEqual(length, 0);
96+
});
97+
98+
it('can queue destroy then save', async () => {
99+
const object = new TestObject({ hash: 'test' });
100+
await Parse.EventuallyQueue.destroy(object);
101+
await Parse.EventuallyQueue.save(object);
102+
await Parse.EventuallyQueue.sendQueue();
103+
104+
const query = new Parse.Query(TestObject);
105+
query.equalTo('hash', 'test');
106+
const results = await query.find();
107+
assert.strictEqual(results.length, 1);
108+
109+
const length = await Parse.EventuallyQueue.length();
110+
assert.strictEqual(length, 0);
111+
});
112+
113+
it('can queue unsaved object with hash', async () => {
114+
const hash = 'secret';
115+
const object = new TestObject({ test: 'test' });
116+
object.set('hash', hash);
117+
await Parse.EventuallyQueue.save(object);
118+
await Parse.EventuallyQueue.sendQueue();
119+
120+
const query = new Parse.Query(TestObject);
121+
query.equalTo('hash', hash);
122+
const results = await query.find();
123+
assert.strictEqual(results.length, 1);
124+
});
125+
126+
it('can queue saved object and unsaved with hash', async () => {
127+
const hash = 'ransom+salt';
128+
const object = new TestObject({ test: 'test' });
129+
object.set('hash', hash);
130+
await Parse.EventuallyQueue.save(object);
131+
await Parse.EventuallyQueue.sendQueue();
132+
133+
let query = new Parse.Query(TestObject);
134+
query.equalTo('hash', hash);
135+
const results = await query.find();
136+
assert.strictEqual(results.length, 1);
137+
138+
const unsaved = new TestObject({ hash, foo: 'bar' });
139+
await Parse.EventuallyQueue.save(unsaved);
140+
await Parse.EventuallyQueue.sendQueue();
141+
142+
query = new Parse.Query(TestObject);
143+
query.equalTo('hash', hash);
144+
const hashes = await query.find();
145+
assert.strictEqual(hashes.length, 1);
146+
assert.strictEqual(hashes[0].get('foo'), 'bar');
147+
});
148+
149+
it('can poll server', async () => {
150+
const object = new TestObject({ test: 'test' });
151+
await object.save();
152+
object.set('foo', 'bar');
153+
await Parse.EventuallyQueue.save(object);
154+
Parse.EventuallyQueue.poll();
155+
assert.ok(Parse.EventuallyQueue.polling);
156+
157+
await sleep(4000);
158+
const query = new Parse.Query(TestObject);
159+
const result = await query.get(object.id);
160+
assert.strictEqual(result.get('foo'), 'bar');
161+
162+
const length = await Parse.EventuallyQueue.length();
163+
assert.strictEqual(length, 0);
164+
assert.strictEqual(Parse.EventuallyQueue.polling, undefined);
165+
});
166+
167+
it('can clear queue', async () => {
168+
const object = new TestObject({ test: 'test' });
169+
await object.save();
170+
await Parse.EventuallyQueue.save(object);
171+
const q = await Parse.EventuallyQueue.getQueue();
172+
assert.strictEqual(q.length, 1);
173+
174+
await Parse.EventuallyQueue.clear();
175+
const length = await Parse.EventuallyQueue.length();
176+
assert.strictEqual(length, 0);
177+
});
178+
});

src/EventuallyQueue.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* https://github.com/francimedia/parse-js-local-storage
3+
*
4+
* @flow
5+
*/
6+
7+
import CoreManager from './CoreManager';
8+
import ParseObject from './ParseObject';
9+
import ParseQuery from './ParseQuery';
10+
import Storage from './Storage';
11+
12+
const EventuallyQueue = {
13+
localStorageKey: 'Parse.Eventually.Queue',
14+
polling: undefined,
15+
16+
save(object) {
17+
return this.enqueue('save', object);
18+
},
19+
20+
destroy(object) {
21+
return this.enqueue('delete', object);
22+
},
23+
24+
async enqueue(action, object) {
25+
const queueData = await this.getQueue();
26+
object._getId();
27+
const { className, id, _localId } = object;
28+
const hash = object.get('hash') || _localId;
29+
const queueId = [action, className, id, hash].join('_');
30+
31+
let index = this.queueItemExists(queueData, queueId);
32+
if (index > -1) {
33+
// Add cached values to new object if they don't exist
34+
for (const prop in queueData[index].object.attributes) {
35+
if (typeof object.get(prop) === 'undefined') {
36+
object.set(prop, queueData[index].object.attributes[prop]);
37+
}
38+
}
39+
} else {
40+
index = queueData.length;
41+
}
42+
queueData[index] = {
43+
id,
44+
queueId,
45+
className,
46+
action,
47+
object,
48+
hash: object.get('hash'),
49+
createdAt: new Date(),
50+
};
51+
return this.setQueue(queueData);
52+
},
53+
54+
async getQueue() {
55+
const q = await Storage.getItemAsync(this.localStorageKey);
56+
if (!q) {
57+
return [];
58+
}
59+
return JSON.parse(q);
60+
},
61+
62+
setQueue(queueData) {
63+
return Storage.setItemAsync(this.localStorageKey, JSON.stringify(queueData));
64+
},
65+
66+
async remove(queueId) {
67+
const queueData = await this.getQueue();
68+
const index = this.queueItemExists(queueData, queueId);
69+
if (index > -1) {
70+
queueData.splice(index, 1);
71+
await this.setQueue(queueData);
72+
}
73+
},
74+
75+
clear() {
76+
return Storage.setItemAsync(this.localStorageKey, JSON.stringify([]));
77+
},
78+
79+
queueItemExists(queueData, queueId) {
80+
return queueData.findIndex(data => data.queueId === queueId);
81+
},
82+
83+
async length() {
84+
const queueData = await this.getQueue();
85+
return queueData.length;
86+
},
87+
88+
async sendQueue(sessionToken) {
89+
const queueData = await this.getQueue();
90+
if (queueData.length === 0) {
91+
return false;
92+
}
93+
for (let i = 0; i < queueData.length; i += 1) {
94+
const ObjectType = ParseObject.extend(queueData[i].className);
95+
if (queueData[i].id) {
96+
await this.reprocess.byId(ObjectType, queueData[i], sessionToken);
97+
} else if (queueData[i].hash) {
98+
await this.reprocess.byHash(ObjectType, queueData[i], sessionToken);
99+
} else {
100+
await this.reprocess.create(ObjectType, queueData[i], sessionToken);
101+
}
102+
}
103+
return true;
104+
},
105+
106+
async sendQueueCallback(object, queueObject, sessionToken) {
107+
if (!object) {
108+
return this.remove(queueObject.queueId);
109+
}
110+
switch (queueObject.action) {
111+
case 'save':
112+
// Queued update was overwritten by other request. Do not save
113+
if (
114+
typeof object.updatedAt !== 'undefined' &&
115+
object.updatedAt > new Date(queueObject.object.createdAt)
116+
) {
117+
return this.remove(queueObject.queueId);
118+
}
119+
try {
120+
await object.save(queueObject.object, { sessionToken });
121+
await this.remove(queueObject.queueId);
122+
} catch (e) {
123+
// Do Nothing
124+
}
125+
break;
126+
case 'delete':
127+
try {
128+
await object.destroy({ sessionToken });
129+
} catch (e) {
130+
// Do Nothing
131+
}
132+
await this.remove(queueObject.queueId);
133+
break;
134+
}
135+
},
136+
137+
poll(sessionToken) {
138+
if (this.polling) {
139+
return;
140+
}
141+
this.polling = setInterval(() => {
142+
let url = CoreManager.get('SERVER_URL');
143+
url += url[url.length - 1] !== '/' ? '/health' : 'health';
144+
145+
const RESTController = CoreManager.getRESTController();
146+
RESTController.ajax('GET', url)
147+
.then(async () => {
148+
clearInterval(this.polling);
149+
delete this.polling;
150+
await this.sendQueue(sessionToken);
151+
})
152+
.catch(() => {
153+
// Can't connect to server, continue
154+
});
155+
}, 2000);
156+
},
157+
158+
reprocess: {
159+
create(ObjectType, queueObject, sessionToken) {
160+
const newObject = new ObjectType();
161+
return EventuallyQueue.sendQueueCallback(newObject, queueObject, sessionToken);
162+
},
163+
async byId(ObjectType, queueObject, sessionToken) {
164+
const query = new ParseQuery(ObjectType);
165+
query.equalTo('objectId', queueObject.id);
166+
const results = await query.find({ sessionToken });
167+
return EventuallyQueue.sendQueueCallback(results[0], queueObject, sessionToken);
168+
},
169+
async byHash(ObjectType, queueObject, sessionToken) {
170+
const query = new ParseQuery(ObjectType);
171+
query.equalTo('hash', queueObject.hash);
172+
const results = await query.find({ sessionToken });
173+
if (results.length > 0) {
174+
return EventuallyQueue.sendQueueCallback(results[0], queueObject, sessionToken);
175+
}
176+
return EventuallyQueue.reprocess.create(ObjectType, queueObject, sessionToken);
177+
},
178+
},
179+
};
180+
181+
module.exports = EventuallyQueue;

src/Parse.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ Parse.CLP = require('./ParseCLP').default;
197197
Parse.CoreManager = require('./CoreManager');
198198
Parse.Config = require('./ParseConfig').default;
199199
Parse.Error = require('./ParseError').default;
200+
Parse.EventuallyQueue = require('./EventuallyQueue');
200201
Parse.FacebookUtils = require('./FacebookUtils').default;
201202
Parse.File = require('./ParseFile').default;
202203
Parse.GeoPoint = require('./ParseGeoPoint').default;

0 commit comments

Comments
 (0)