Skip to content

Commit 28999b9

Browse files
committed
Add saveEventually and destroyEventually
1 parent 11738f5 commit 28999b9

File tree

10 files changed

+398
-84
lines changed

10 files changed

+398
-84
lines changed

integration/test/ParseEventuallyQueueTest.js

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
'use strict';
22

33
const assert = require('assert');
4-
const clear = require('./clear');
54
const Parse = require('../../node');
65
const sleep = require('./sleep');
76

8-
const TestObject = Parse.Object.extend('TestObject');
9-
107
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-
208
it('can queue save object', async () => {
219
const object = new TestObject({ test: 'test' });
2210
await object.save();
@@ -175,4 +163,55 @@ describe('Parse EventuallyQueue', () => {
175163
const length = await Parse.EventuallyQueue.length();
176164
assert.strictEqual(length, 0);
177165
});
166+
167+
it('can saveEventually', async done => {
168+
const parseServer = await reconfigureServer();
169+
const object = new TestObject({ hash: 'saveSecret' });
170+
await parseServer.handleShutdown();
171+
parseServer.server.close(async () => {
172+
await object.saveEventually();
173+
let length = await Parse.EventuallyQueue.length();
174+
assert(Parse.EventuallyQueue.polling);
175+
assert.strictEqual(length, 1);
176+
177+
await reconfigureServer({});
178+
await sleep(3000); // Wait for polling
179+
180+
assert.strictEqual(Parse.EventuallyQueue.polling, undefined);
181+
length = await Parse.EventuallyQueue.length();
182+
assert.strictEqual(length, 0);
183+
184+
const query = new Parse.Query(TestObject);
185+
query.equalTo('hash', 'saveSecret');
186+
const results = await query.find();
187+
assert.strictEqual(results.length, 1);
188+
done();
189+
});
190+
});
191+
192+
it('can destroyEventually', async done => {
193+
const parseServer = await reconfigureServer();
194+
const object = new TestObject({ hash: 'deleteSecret' });
195+
await object.save();
196+
await parseServer.handleShutdown();
197+
parseServer.server.close(async () => {
198+
await object.destroyEventually();
199+
let length = await Parse.EventuallyQueue.length();
200+
assert(Parse.EventuallyQueue.polling);
201+
assert.strictEqual(length, 1);
202+
203+
await reconfigureServer({});
204+
await sleep(3000); // Wait for polling
205+
206+
assert.strictEqual(Parse.EventuallyQueue.polling, undefined);
207+
length = await Parse.EventuallyQueue.length();
208+
assert.strictEqual(length, 0);
209+
210+
const query = new Parse.Query(TestObject);
211+
query.equalTo('hash', 'deleteSecret');
212+
const results = await query.find();
213+
assert.strictEqual(results.length, 0);
214+
done();
215+
});
216+
});
178217
});

integration/test/helper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
1+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
22

33
const ParseServer = require('parse-server').default;
44
const CustomAuth = require('./CustomAuth');

jasmine.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"*Test.js"
88
],
99
"random": false,
10-
"timeout": 5000
10+
"timeout": 10000
1111
}

src/EventuallyQueue.js

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,22 @@ const EventuallyQueue = {
1313
localStorageKey: 'Parse.Eventually.Queue',
1414
polling: undefined,
1515

16-
save(object) {
17-
return this.enqueue('save', object);
16+
save(object, serverOptions = {}) {
17+
return this.enqueue('save', object, serverOptions);
1818
},
1919

20-
destroy(object) {
21-
return this.enqueue('delete', object);
20+
destroy(object, serverOptions = {}) {
21+
return this.enqueue('destroy', object, serverOptions);
2222
},
23-
24-
async enqueue(action, object) {
25-
const queueData = await this.getQueue();
23+
generateQueueId(action, object) {
2624
object._getId();
2725
const { className, id, _localId } = object;
28-
const hash = object.get('hash') || _localId;
29-
const queueId = [action, className, id, hash].join('_');
26+
const uniqueId = object.get('hash') || _localId;
27+
return [action, className, id, uniqueId].join('_');
28+
},
29+
async enqueue(action, object, serverOptions) {
30+
const queueData = await this.getQueue();
31+
const queueId = this.generateQueueId(action, object);
3032

3133
let index = this.queueItemExists(queueData, queueId);
3234
if (index > -1) {
@@ -40,11 +42,12 @@ const EventuallyQueue = {
4042
index = queueData.length;
4143
}
4244
queueData[index] = {
43-
id,
4445
queueId,
45-
className,
4646
action,
4747
object,
48+
serverOptions,
49+
id: object.id,
50+
className: object.className,
4851
hash: object.get('hash'),
4952
createdAt: new Date(),
5053
};
@@ -85,25 +88,25 @@ const EventuallyQueue = {
8588
return queueData.length;
8689
},
8790

88-
async sendQueue(sessionToken) {
91+
async sendQueue() {
8992
const queueData = await this.getQueue();
9093
if (queueData.length === 0) {
9194
return false;
9295
}
9396
for (let i = 0; i < queueData.length; i += 1) {
9497
const ObjectType = ParseObject.extend(queueData[i].className);
9598
if (queueData[i].id) {
96-
await this.reprocess.byId(ObjectType, queueData[i], sessionToken);
99+
await this.reprocess.byId(ObjectType, queueData[i]);
97100
} else if (queueData[i].hash) {
98-
await this.reprocess.byHash(ObjectType, queueData[i], sessionToken);
101+
await this.reprocess.byHash(ObjectType, queueData[i]);
99102
} else {
100-
await this.reprocess.create(ObjectType, queueData[i], sessionToken);
103+
await this.reprocess.create(ObjectType, queueData[i]);
101104
}
102105
}
103106
return true;
104107
},
105108

106-
async sendQueueCallback(object, queueObject, sessionToken) {
109+
async sendQueueCallback(object, queueObject) {
107110
if (!object) {
108111
return this.remove(queueObject.queueId);
109112
}
@@ -117,63 +120,67 @@ const EventuallyQueue = {
117120
return this.remove(queueObject.queueId);
118121
}
119122
try {
120-
await object.save(queueObject.object, { sessionToken });
123+
await object.save(queueObject.object, queueObject.serverOptions);
121124
await this.remove(queueObject.queueId);
122125
} catch (e) {
123-
// Do Nothing
126+
if (e.message !== 'XMLHttpRequest failed: "Unable to connect to the Parse API"') {
127+
await this.remove(queueObject.queueId);
128+
}
124129
}
125130
break;
126-
case 'delete':
131+
case 'destroy':
127132
try {
128-
await object.destroy({ sessionToken });
133+
await object.destroy(queueObject.serverOptions);
134+
await this.remove(queueObject.queueId);
129135
} catch (e) {
130-
// Do Nothing
136+
if (e.message !== 'XMLHttpRequest failed: "Unable to connect to the Parse API"') {
137+
await this.remove(queueObject.queueId);
138+
}
131139
}
132-
await this.remove(queueObject.queueId);
133140
break;
134141
}
135142
},
136143

137-
poll(sessionToken) {
144+
poll() {
138145
if (this.polling) {
139146
return;
140147
}
141148
this.polling = setInterval(() => {
142-
let url = CoreManager.get('SERVER_URL');
143-
url += url[url.length - 1] !== '/' ? '/health' : 'health';
144-
145149
const RESTController = CoreManager.getRESTController();
146-
RESTController.ajax('GET', url)
147-
.then(async () => {
150+
RESTController.ajax('GET', CoreManager.get('SERVER_URL')).catch(error => {
151+
if (error !== 'Unable to connect to the Parse API') {
148152
clearInterval(this.polling);
149-
delete this.polling;
150-
await this.sendQueue(sessionToken);
151-
})
152-
.catch(() => {
153-
// Can't connect to server, continue
154-
});
153+
this.polling = undefined;
154+
return this.sendQueue();
155+
}
156+
});
155157
}, 2000);
156158
},
157-
159+
stopPoll() {
160+
clearInterval(this.polling);
161+
this.polling = undefined;
162+
},
158163
reprocess: {
159-
create(ObjectType, queueObject, sessionToken) {
164+
create(ObjectType, queueObject) {
160165
const newObject = new ObjectType();
161-
return EventuallyQueue.sendQueueCallback(newObject, queueObject, sessionToken);
166+
return EventuallyQueue.sendQueueCallback(newObject, queueObject);
162167
},
163-
async byId(ObjectType, queueObject, sessionToken) {
168+
async byId(ObjectType, queueObject) {
169+
const { sessionToken } = queueObject.serverOptions;
164170
const query = new ParseQuery(ObjectType);
165171
query.equalTo('objectId', queueObject.id);
166172
const results = await query.find({ sessionToken });
167-
return EventuallyQueue.sendQueueCallback(results[0], queueObject, sessionToken);
173+
return EventuallyQueue.sendQueueCallback(results[0], queueObject);
168174
},
169-
async byHash(ObjectType, queueObject, sessionToken) {
175+
async byHash(ObjectType, queueObject) {
176+
const { sessionToken } = queueObject.serverOptions;
170177
const query = new ParseQuery(ObjectType);
171178
query.equalTo('hash', queueObject.hash);
172179
const results = await query.find({ sessionToken });
173180
if (results.length > 0) {
174-
return EventuallyQueue.sendQueueCallback(results[0], queueObject, sessionToken);
181+
return EventuallyQueue.sendQueueCallback(results[0], queueObject);
175182
}
176-
return EventuallyQueue.reprocess.create(ObjectType, queueObject, sessionToken);
183+
return EventuallyQueue.reprocess.create(ObjectType, queueObject);
177184
},
178185
},
179186
};

src/Parse.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import decode from './decode';
1111
import encode from './encode';
1212
import CoreManager from './CoreManager';
1313
import CryptoController from './CryptoController';
14+
import EventuallyQueue from './EventuallyQueue';
1415
import InstallationController from './InstallationController';
1516
import * as ParseOp from './ParseOp';
1617
import RESTController from './RESTController';
@@ -46,6 +47,7 @@ const Parse = {
4647
/* eslint-enable no-console */
4748
}
4849
Parse._initialize(applicationId, javaScriptKey);
50+
EventuallyQueue.poll();
4951
},
5052

5153
_initialize(applicationId: string, javaScriptKey: string, masterKey: string) {
@@ -197,7 +199,7 @@ Parse.CLP = require('./ParseCLP').default;
197199
Parse.CoreManager = require('./CoreManager');
198200
Parse.Config = require('./ParseConfig').default;
199201
Parse.Error = require('./ParseError').default;
200-
Parse.EventuallyQueue = require('./EventuallyQueue');
202+
Parse.EventuallyQueue = EventuallyQueue;
201203
Parse.FacebookUtils = require('./FacebookUtils').default;
202204
Parse.File = require('./ParseFile').default;
203205
Parse.GeoPoint = require('./ParseGeoPoint').default;

src/ParseObject.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import canBeSerialized from './canBeSerialized';
1414
import decode from './decode';
1515
import encode from './encode';
1616
import escape from './escape';
17+
import EventuallyQueue from './EventuallyQueue';
1718
import ParseACL from './ParseACL';
1819
import parseDate from './parseDate';
1920
import ParseError from './ParseError';
@@ -1200,6 +1201,42 @@ class ParseObject {
12001201
return this.fetch(options);
12011202
}
12021203

1204+
/**
1205+
* Saves this object to the server at some unspecified time in the future,
1206+
* even if Parse is currently inaccessible.
1207+
*
1208+
* Use this when you may not have a solid network connection, and don't need to know when the save completes.
1209+
* If there is some problem with the object such that it can't be saved, it will be silently discarded.
1210+
*
1211+
* Objects saved with this method will be stored locally in an on-disk cache until they can be delivered to Parse.
1212+
* They will be sent immediately if possible. Otherwise, they will be sent the next time a network connection is
1213+
* available. Objects saved this way will persist even after the app is closed, in which case they will be sent the
1214+
* next time the app is opened.
1215+
*
1216+
* @param {object} [options]
1217+
* Used to pass option parameters to method if arg1 and arg2 were both passed as strings.
1218+
* Valid options are:
1219+
* <ul>
1220+
* <li>sessionToken: A valid session token, used for making a request on
1221+
* behalf of a specific user.
1222+
* <li>cascadeSave: If `false`, nested objects will not be saved (default is `true`).
1223+
* <li>context: A dictionary that is accessible in Cloud Code `beforeSave` and `afterSave` triggers.
1224+
* </ul>
1225+
* @returns {Promise} A promise that is fulfilled when the save
1226+
* completes.
1227+
*/
1228+
async saveEventually(options: SaveOptions): Promise {
1229+
try {
1230+
await this.save(null, options);
1231+
} catch (e) {
1232+
if (e.message === 'XMLHttpRequest failed: "Unable to connect to the Parse API"') {
1233+
await EventuallyQueue.save(this, options);
1234+
EventuallyQueue.poll();
1235+
}
1236+
}
1237+
return this;
1238+
}
1239+
12031240
/**
12041241
* Set a hash of model attributes, and save the model to the server.
12051242
* updatedAt will be updated when the request returns.
@@ -1305,6 +1342,40 @@ class ParseObject {
13051342
});
13061343
}
13071344

1345+
/**
1346+
* Deletes this object from the server at some unspecified time in the future,
1347+
* even if Parse is currently inaccessible.
1348+
*
1349+
* Use this when you may not have a solid network connection,
1350+
* and don't need to know when the delete completes. If there is some problem with the object
1351+
* such that it can't be deleted, the request will be silently discarded.
1352+
*
1353+
* Delete instructions made with this method will be stored locally in an on-disk cache until they can be transmitted
1354+
* to Parse. They will be sent immediately if possible. Otherwise, they will be sent the next time a network connection
1355+
* is available. Delete requests will persist even after the app is closed, in which case they will be sent the
1356+
* next time the app is opened.
1357+
*
1358+
* @param {object} [options]
1359+
* Valid options are:<ul>
1360+
* <li>sessionToken: A valid session token, used for making a request on
1361+
* behalf of a specific user.
1362+
* <li>context: A dictionary that is accessible in Cloud Code `beforeDelete` and `afterDelete` triggers.
1363+
* </ul>
1364+
* @returns {Promise} A promise that is fulfilled when the destroy
1365+
* completes.
1366+
*/
1367+
async destroyEventually(options: RequestOptions): Promise {
1368+
try {
1369+
await this.destroy(options);
1370+
} catch (e) {
1371+
if (e.message === 'XMLHttpRequest failed: "Unable to connect to the Parse API"') {
1372+
await EventuallyQueue.destroy(this, options);
1373+
EventuallyQueue.poll();
1374+
}
1375+
}
1376+
return this;
1377+
}
1378+
13081379
/**
13091380
* Destroy this model on the server if it was already persisted.
13101381
*

0 commit comments

Comments
 (0)