Skip to content

Commit f5d1d57

Browse files
authored
Fix: Stop rebasing sent changes when receiving remote updates (#78)
See prior version in #75 - this is a more formal change When we have sent changes for an entity and are waiting for server's response we might first receive updates from the server that represent changes which another client made to the same entity. In those cases we have been forgetting about the changes we sent and then transforming our local changes to the new updates. To our client it appears like we have made changes locally on top of the remote changes when in reality we only made changes to the previous base version of the entity. The result of this forgetfulness is that we replay changes that should only happen once. This might appear in the form of duplicated content but could expose itself in other forms. In this patch we're guarding the transformation based on the presence of those waiting changes. If we get updates while waiting, simply apply those updates to our internal ghost and let the client handle our original submission once it comes back. If it comes back confirmed by the server we'll be able to apply the updated patch which the server created. If it gets rejected we will end up sending the full copy of our changes and mess up the note but the original data should remian in the history. This patch includes a test which demonstrates the sequence that produces the issue and verifies that it doesn't return. Props to @beaucollins for his work figuring this stuff out.
1 parent 8bdb76b commit f5d1d57

File tree

3 files changed

+53
-1
lines changed

3 files changed

+53
-1
lines changed

RELEASE-NOTES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## Future Release
4+
5+
- Fixed data corruption when remote updates arrive while waiting on confirmation of local changes [#78](https://github.com/Simperium/node-simperium/pull/78)

src/simperium/channel.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ internal.updateObjectVersion = function( id, version, data, original, patch, ack
116116
// If it's not an ack, it's a change initiated on a different client
117117
// we need to provide a way for the current client to respond to
118118
// a potential conflict if it has modifications that have not been synced
119-
if ( !acknowledged ) {
119+
if ( !acknowledged && !this.localQueue.sent[id] ) {
120120
changes = this.localQueue.dequeueChangesFor( id );
121121
localModifications = change_util.compressChanges( changes, original );
122122
remoteModifications = patch;

test/simperium/channel_test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,53 @@ describe( 'Channel', function() {
357357
bucket.update( key, {title: 'Goodbye world'} );
358358
} ) );
359359

360+
it( 'should not rebase local changes waiting for confirmation from the server', async () => {
361+
await channel.store.put( 'thing', 1, { content: 'AC' } );
362+
363+
channel.localQueue.queue( {
364+
id: 'thing',
365+
o: 'M',
366+
sv: 1,
367+
ev: 2,
368+
ccid: 'local',
369+
v: diff( { content: 'AC' }, { content: 'ACD' } )
370+
} );
371+
372+
await new Promise( resolve => channel.once( 'send', resolve ) );
373+
channel.once( 'send', () => done( 'Should not re-send changes which are already outbound' ) );
374+
375+
const ghost = await channel.store.get( 'thing' );
376+
deepEqual( ghost.data, { content: 'AC' } );
377+
378+
channel.handleMessage( 'c:' + JSON.stringify( [ {
379+
id: 'thing',
380+
o: 'M',
381+
sv: 1,
382+
ev: 2,
383+
ccids: [ 'remote' ],
384+
v: diff( { content: 'AC' }, { content: 'ABC' } )
385+
} ] ) );
386+
387+
await channel.store.get( 'thing' );
388+
389+
return new Promise( resolve => {
390+
channel.once( 'acknowledge', async () => {
391+
const ghost = await channel.store.get( 'thing' );
392+
deepEqual( ghost.data, { content: 'ABCD' } );
393+
resolve();
394+
} );
395+
396+
channel.handleMessage( 'c:' + JSON.stringify( [ {
397+
id: 'thing',
398+
o: 'M',
399+
sv: 2,
400+
ev: 3,
401+
ccids: [ 'local' ],
402+
v: diff( { content: 'ABC' }, { content: 'ABCD' } )
403+
} ] ) )
404+
} );
405+
} );
406+
360407
it( 'should emit errors on the bucket instance', ( done ) => {
361408
const error = {error: 404, id: 'thing', ccids: ['abc']}
362409
bucket.on( 'error', ( e ) => {

0 commit comments

Comments
 (0)