Skip to content

Commit a133732

Browse files
beaucollinsdmsnell
authored andcommitted
Overwritten Ghost (#61)
* Adds a unit test to simulate a change queue being emptied when application restarts Local modifications end up overwriting the network changes instead of merging the changes * Refactor the updateObjectVersion function and document what it does * Stubs out a method to use during a network change - Fixes test that changes due to additional async call, the sent change will be the rebased patch * Adds subscribe method to bucket and channel to support handling network changes * Adds bucket implementation for subscriber interface - adds unit tests for bucket * Update jsdoc for NetworkChangeSubscriber return value * Remove console log * typos * Fixing things * Update test so there is no sent change pending * subscribe is now beforeNetworkChange Instance names, arguments, methods have been changed to more clearly reflect the purpose of the API * Add release notes and version bump * Fix Channel change processor to always use the latest Ghost (#84) * Always produce diffs against the most up to date ghost * Flow check on the CI * Don't track sent change unless it is actually sent * Test: Crossed Wires scenario (#86) * Integration test for 'crossed wires' problem * Fix: stop skipping updates when local changes queued
1 parent f5d1d57 commit a133732

File tree

13 files changed

+514
-161
lines changed

13 files changed

+514
-161
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ script:
55
- npm run lint
66
# unit tests with coverage report
77
- npm run test:coverage
8+
# Flow type check
9+
- npx flow
810
node_js:
911
- "10"
1012
- "9"

RELEASE-NOTES.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22

33
## Future Release
44

5-
- Fixed data corruption when remote updates arrive while waiting on confirmation of local changes [#78](https://github.com/Simperium/node-simperium/pull/78)
5+
### Fixes
6+
7+
- Prevent data corruption when remote updates arrive while waiting on confirmation of local changes [#78](https://github.com/Simperium/node-simperium/pull/78)
8+
- Prevent data loss when receiving remote updates while local changes exist [#61](https://github.com/Simperium/node-simperium/pull/61)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "simperium",
3-
"version": "0.3.3",
3+
"version": "1.0.0",
44
"description": "A simperium client for node.js",
55
"main": "./lib/simperium/index.js",
66
"repository": {

src/simperium/auth.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type User = {
1010
};
1111

1212
const fromJSON = ( json: string ): User => {
13-
const data: {} = JSON.parse( json );
13+
const data = JSON.parse( json );
1414
if ( ! data.access_token && typeof data.access_token !== 'string' ) {
1515
throw new Error( 'access_token not present' );
1616
}
@@ -74,9 +74,10 @@ export class Auth extends EventEmitter {
7474
}
7575

7676
getUrlOptions( path: string ) {
77-
const options = url.parse( `${URL}/${ this.appId }/${ path}` );
77+
const { port, ...options } = url.parse( `${URL}/${ this.appId }/${ path}` );
7878
return {
7979
... options,
80+
port: port ? Number( port ) : undefined,
8081
method: 'POST',
8182
headers: {'X-Simperium-API-Key': this.appSecret }
8283
};

src/simperium/bucket.js

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,64 @@ Bucket.prototype.setChannel = function( channel ) {
196196
.on( 'update', this.onChannelUpdate )
197197
.on( 'indexingStateChange', this.onChannelIndexingStateChange )
198198
.on( 'remove', this.onChannelRemove );
199+
200+
/**
201+
* Fetch the current state for a given id in the datastore
202+
*
203+
* @param {string} id - object to fetch current state for
204+
* @returns {Promise<Object|null>} local state or null if no state to provide
205+
*/
206+
const localStateForKey = ( id ) => {
207+
return this.get( id ).then( ( object ) => {
208+
if ( object ) {
209+
return object.data;
210+
}
211+
} );
212+
}
213+
214+
// Sets up a default network change subscriber that triggers a channel
215+
// change operation. This will queue up any changes that exist that
216+
// may have not been added to the local queue yet
217+
channel.beforeNetworkChange( ( id, ... args ) => {
218+
const changeResolver = this.changeResolver
219+
// if there is a subcriber, let it handle the network change
220+
? Promise.resolve( this.changeResolver( id, ... args ) )
221+
// if no subscriber, resolving null will mean we should handle it
222+
: Promise.resolve( null );
223+
224+
return changeResolver
225+
.then( localState => {
226+
// if subscriber provided local state of any truthy type use that
227+
// as the object's local state
228+
if ( localState ) {
229+
return localState;
230+
}
231+
// Subscriber did not provide local state, fetch it from the datastore
232+
return localStateForKey( id );
233+
} )
234+
} );
199235
};
200236

237+
/**
238+
* @callback NetworkChangeResolver
239+
* @param { string } key - bucket object being changed
240+
* @param { Object } data - the new object data
241+
* @param { Object } base - the object data before the patch is applied
242+
* @param { Object } patch - the patch used to bring base to data
243+
* @returns { Object | null | Promise<Object | null> } - resolve when network change should be applied
244+
*/
245+
246+
/**
247+
* Subscribe to changes to this bucket that are coming from simperium that
248+
* did not originate on this client. Can be used to save an object before
249+
* network changes are applied.
250+
*
251+
* @param {NetworkChangeResolver} changeResolver - callback executed when network changes for this bucket are going to be applied
252+
*/
253+
Bucket.prototype.beforeNetworkChange = function( changeResolver ) {
254+
this.changeResolver = changeResolver;
255+
}
256+
201257
/**
202258
* Reloads all the data from the currently cached set of ghost data
203259
*/
@@ -258,9 +314,11 @@ Bucket.prototype.update = function( id, data, remoteUpdateInfo, options, callbac
258314
}
259315

260316
const task = this.storeAPI.update( id, data, this.isIndexing )
317+
.then( bucketObject => {
318+
return this.channel.update( bucketObject, options.sync );
319+
} )
261320
.then( bucketObject => {
262321
this.emit( 'update', id, bucketObject.data, remoteUpdateInfo );
263-
this.channel.update( bucketObject, options.sync );
264322
return bucketObject;
265323
} );
266324
return deprecateCallback( callback, task );
@@ -307,11 +365,15 @@ Bucket.prototype.getVersion = function( id, callback ) {
307365
*
308366
* @param {String} id - object to sync
309367
* @param {?bucketStoreGetCallback} callback - optional callback
310-
* @returns {Promise<Object>} - object id, data
368+
* @returns {Promise<BucketObject>} - object id, data
311369
*/
312370
Bucket.prototype.touch = function( id, callback ) {
313371
const task = this.storeAPI.get( id )
314-
.then( object => this.update( object.id, object.data ) );
372+
.then( object => {
373+
if ( object ) {
374+
return this.update( object.id, object.data );
375+
}
376+
} );
315377

316378
return deprecateCallback( callback, task );
317379
};

0 commit comments

Comments
 (0)