From 9912b9ce29797ffc77aaf4669f6e575a9b74bc88 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Thu, 22 Feb 2018 11:51:23 -0800 Subject: [PATCH 1/8] Updates development dependencies --- package.json | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 7e3e991..f6d9b4b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "url": "git://github.com/Simperium/node-simperium.git" }, "scripts": { - "test": "mocha --compilers js:babel-core/register --require test/helper test/**", + "flow": "flow", + "test": "mocha --require babel-core/register --require test/helper test/**", "prepublish": "babel -q -d lib/ src/" }, "author": "", @@ -18,11 +19,12 @@ "websocket": "^1.0.22" }, "devDependencies": { - "babel-cli": "^6.2.0", - "babel-core": "^6.2.0", - "babel-eslint": "^8.2.1", - "babel-preset-es2015": "^6.2.0", - "eslint": "^4.17.0", - "mocha": "^2.3.4" + "babel-cli": "^6.26.0", + "babel-core": "^6.26.0", + "babel-eslint": "^8.2.2", + "babel-preset-es2015": "^6.24.1", + "eslint": "^4.18.1", + "flow-bin": "^0.66.0", + "mocha": "^5.0.1" } } From 7ad27005069028bf704e2073bc3146814d5729fd Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Thu, 22 Feb 2018 13:59:47 -0800 Subject: [PATCH 2/8] Adds flowtype to Channel module. Fixes type errors - Changes 1 codepath for handling simperium change error responses differently than valid change responses --- .babelrc | 2 +- package.json | 1 + src/simperium/channel.js | 163 +++++++++++++++++++++++++++------ test/simperium/channel_test.js | 14 +-- 4 files changed, 144 insertions(+), 36 deletions(-) diff --git a/.babelrc b/.babelrc index 8a87b4d..019a465 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["es2015"] + "presets": ["es2015", "flow"] } \ No newline at end of file diff --git a/package.json b/package.json index f6d9b4b..e8ce415 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "babel-core": "^6.26.0", "babel-eslint": "^8.2.2", "babel-preset-es2015": "^6.24.1", + "babel-preset-flow": "^6.23.0", "eslint": "^4.18.1", "flow-bin": "^0.66.0", "mocha": "^5.0.1" diff --git a/src/simperium/channel.js b/src/simperium/channel.js index 5cb5403..0b891c7 100644 --- a/src/simperium/channel.js +++ b/src/simperium/channel.js @@ -1,16 +1,62 @@ +// @flow /*eslint no-shadow: 0*/ import { format, inherits } from 'util' -import { EventEmitter } from 'events' +import events from 'events' import { parseMessage, parseVersionMessage, change as change_util } from './util' import JSONDiff from './jsondiff' import { v4 as uuid } from 'uuid' +const { EventEmitter } = events; + const jsondiff = new JSONDiff( {list_diff: false} ); +type LocalChange = { + id: string, + ccid: string +} + +type NetworkChange = { + id: string, + ccids: string[], + o: '+' | '-' | 'M', + sv: number, + ev: number, + cv: string, + v?: {} +} + +type NetworkChangeErrorResponse = { + error: number, + id: string, + ccids: string[], + d: ?{}, + hasSentFullObject?: boolean +}; + +type Ghost = { + key: string, + version: number, + data: {} +} + +class ChangeError extends Error { + code: number; + changeError: NetworkChangeErrorResponse; + change: ?LocalChange; + + constructor( changeError: NetworkChangeErrorResponse, localChange: ?LocalChange ) { + super( `${changeError.error} - Could not apply change to: ${changeError.id}` ); + this.code = changeError.error; + this.changeError = changeError; + this.change = localChange; + } +} + const UNKNOWN_CV = '?'; const CODE_INVALID_VERSION = 405; const CODE_EMPTY_RESPONSE = 412; const CODE_INVALID_DIFF = 440; +const CODE_DUPLICATE_CHANGE = 409; const operation = { MODIFY: 'M', @@ -41,7 +87,7 @@ internal.updateChangeVersion = function( cv ) { * @param {String} id - id of the object changed * @param {Object} change - the change to apply to the object */ -internal.changeObject = function( id, change ) { +internal.changeObject = function( id: string, change: NetworkChange ) { // pull out the object from the store and apply the change delta var applyChange = internal.performChange.bind( this, change ); @@ -153,7 +199,7 @@ internal.removeObject = function( id, acknowledged ) { return this.store.remove( id ).then( notify ); }; -internal.updateAcknowledged = function( change ) { +internal.updateAcknowledged = function( change: LocalChange ) { var id = change.id; if ( this.localQueue.sent[id] === change ) { this.localQueue.acknowledge( change ); @@ -166,7 +212,7 @@ internal.performChange = function( change ) { return this.store.get( change.id ).then( success ); }; -internal.findAcknowledgedChange = function( change ) { +internal.findAcknowledgedChange = function( change: { id: string, ccids: string[] } ) { var possibleChange = this.localQueue.sent[change.id]; if ( possibleChange ) { if ( ( change.ccids || [] ).indexOf( possibleChange.ccid ) > -1 ) { @@ -184,12 +230,21 @@ internal.requestObjectVersion = function( id, version ) { } ); }; -internal.applyChange = function( change, ghost ) { - const acknowledged = internal.findAcknowledgedChange.bind( this )( change ), +const applyChangeError = ( channel: Channel, changeError: NetworkChangeErrorResponse ) => { + // run on network queue for the relevant bucket object + channel.networkQueue.queueFor( changeError.id ).add( ( done ) => { + const localChange = internal.findAcknowledgedChange.call( channel, changeError ); + const error = new ChangeError( changeError, localChange ); + internal.handleChangeError.call( channel, error, changeError, localChange ); + done(); + } ) +}; + +internal.applyChange = function( change: NetworkChange, ghost: Ghost ) { + const acknowledged = internal.findAcknowledgedChange.call( this, change ), updateChangeVersion = internal.updateChangeVersion.bind( this, change.cv ); - let error, - original, + let original, patch, modified; // attempt to apply the change @@ -199,19 +254,13 @@ internal.applyChange = function( change, ghost ) { // id: '9e9a9616-8174-425a-a1b0-9ed5410f1edc', // clientid: 'node-b9776e96-c068-42ae-893a-03f50833bddb', // error: 400 } - if ( change.error ) { - error = new Error( `${change.error} - Could not apply change to: ${ghost.key}` ); - error.code = change.error; - error.change = change; - error.ghost = ghost; - internal.handleChangeError.call( this, error, change, acknowledged ); - return; - } if ( change.o === operation.MODIFY ) { - if ( ghost && ( ghost.version !== change.sv ) ) { + const matchesStartingVersion = change.sv === ghost.version || + ( change.sv === 0 && ( ghost.version === null || ghost.version === undefined ) ); + if ( ! matchesStartingVersion ) { internal.requestObjectVersion.call( this, change.id, change.sv ).then( data => { - internal.applyChange.call( this, change, { version: change.sv, data } ) + internal.applyChange.call( this, change, { key: ghost.key, version: change.sv, data } ) } ); return; } @@ -226,8 +275,12 @@ internal.applyChange = function( change, ghost ) { } } -internal.handleChangeError = function( err, change, acknowledged ) { +internal.handleChangeError = function( err: ChangeError, change: NetworkChangeErrorResponse, acknowledged: ?LocalChange ) { switch ( err.code ) { + case CODE_DUPLICATE_CHANGE: + if ( ! acknowledged ) { + break; + } case CODE_INVALID_VERSION: case CODE_INVALID_DIFF: // Invalid version or diff, send full object back to server if ( ! change.hasSentFullObject ) { @@ -242,7 +295,9 @@ internal.handleChangeError = function( err, change, acknowledged ) { break; case CODE_EMPTY_RESPONSE: // Change causes no change, just acknowledge it - internal.updateAcknowledged.call( this, acknowledged ); + if ( acknowledged ) { + internal.updateAcknowledged.call( this, acknowledged ); + } break; default: this.emit( 'error', err, change ); @@ -291,7 +346,7 @@ internal.indexingComplete = function() { * * @interface GhostStore */ - +interface GhostStore { /** * Retrieve a Ghost for the given bucket object id * @@ -344,7 +399,7 @@ internal.indexingComplete = function() { * @name GhostStore#setChangeVersion * @returns {Promise} - resolves once the change version is saved */ - +} /** * Maintains syncing state for a Simperium bucket. @@ -364,7 +419,7 @@ internal.indexingComplete = function() { * @param {GhostStore} store - data storage for ghost objects * @param {String} name - the name of the bucket on Simperium.com */ -export default function Channel( appid, access_token, store, name ) { +export default function Channel( appid: string, access_token: string, store: GhostStore, name: string ) { // Uses an event emitter to handle different Simperium commands const message = this.message = new EventEmitter(); @@ -634,12 +689,63 @@ Channel.prototype.sendChangeVersionRequest = function( cv ) { this.send( format( 'cv:%s', cv ) ); }; + +type ChangeMessage = { + clientid?: string, + ccids?: string[], + id?: string, // Bucket object being changed + o?: string, // TODO: force into valid operation types, + v?: {}, // TODO: force into valid change value + cv?: string, // Bucket cv this change represents + sv?: number, + ev?: number, + error?: number, + d?: {} +}; + +const requireProp = ( key: string, object: {} ): T => { + const value = object[ key ]; + if ( value ) { + return value; + } + throw new Error( `unexpected value ${ value } for key ${ key } in ${ JSON.stringify( object ) }` ); +} + +const asNetworkErrorResponse = ( changeMessage: ChangeMessage ): NetworkChangeErrorResponse => { + return { + id: requireProp( 'id', changeMessage ), + d: changeMessage.d, + ccids: requireProp( 'ccids', changeMessage ), + error: requireProp( 'error', changeMessage ) + }; +} + +const asNetworkChange = ( changeMessage: ChangeMessage ): NetworkChange => { + return { + id: requireProp( 'id', changeMessage ), + ccids: requireProp( 'ccids', changeMessage ), + o: requireProp( 'o', changeMessage ), + d: changeMessage.id, + cv: requireProp( 'cv', changeMessage ), + ev: requireProp( 'ev', changeMessage ), + sv: changeMessage.sv ? changeMessage.sv : 0, + v: changeMessage.v // v not required for `-` change types + }; +} + Channel.prototype.onChanges = function( data ) { var changes = JSON.parse( data ), onChange = internal.changeObject.bind( this ); - changes.forEach( function( change ) { - onChange( change.id, change ); + changes.forEach( ( change ) => { + // TODO: parsed change _could_ be an error response + // error change shape an non-error change shape are _not_ the same + // do validation here and choose appropriate code path + if ( change.error ) { + applyChangeError( this, asNetworkErrorResponse( change ) ); + } else { + onChange( change.id, asNetworkChange( change ) ); + } } ); // emit ready after all server changes have been applied this.emit( 'ready' ); @@ -866,7 +972,7 @@ LocalQueue.prototype.resendSentChanges = function() { * * @type {Map} stores specific revisions as a cache */ -export const revisionCache = new Map(); +export const revisionCache: Map = new Map(); /** * Attempts to fetch an entity's revisions @@ -966,8 +1072,9 @@ function collectionRevisions( channel, id, callback ) { requestedVersions.add( version ); // fetch from server or local cache - if ( revisionCache.has( `${ id }.${ version }` ) ) { - onVersion( id, version, revisionCache.get( `${ id }.${ version }` ) ); + const cached = revisionCache.get( `${ id }.${ version }` ); + if ( cached ) { + onVersion( id, version, cached ); } else { channel.send( `e:${ id }.${ version }` ); } diff --git a/test/simperium/channel_test.js b/test/simperium/channel_test.js index dd57405..fe501a0 100644 --- a/test/simperium/channel_test.js +++ b/test/simperium/channel_test.js @@ -77,9 +77,9 @@ describe( 'Channel', function() { version1 = { content: 'step 1'}, version2 = { content: 'step 2'}, version3 = { content: 'step 3'}, - change1 = { o: 'M', ev: 1, cv: 'cv1', id: id, v: diff( {}, version1 )}, - change2 = { o: 'M', ev: 2, sv: 1, cv: 'cv2', id: id, v: diff( version1, version2 )}, - change3 = { o: 'M', ev: 3, sv: 2, cv: 'cv3', id: id, v: diff( version2, version3 )}, + change1 = { o: 'M', ccids: [], ev: 1, cv: 'cv1', id: id, v: diff( {}, version1 )}, + change2 = { o: 'M', ccids: [], ev: 2, sv: 1, cv: 'cv2', id: id, v: diff( version1, version2 )}, + change3 = { o: 'M', ccids: [], ev: 3, sv: 2, cv: 'cv3', id: id, v: diff( version2, version3 )}, check = fn.counts( 2, function( id, data ) { equal( data.content, 'step 3' ); done(); @@ -238,7 +238,7 @@ describe( 'Channel', function() { it( 'should notify bucket after receiving a network change', () => { const id = 'object', data = { content: 'step 1'}, - change = { o: 'M', ev: 1, cv: 'cv1', id: id, v: diff( {}, data )}; + change = { o: 'M', ccids: [], ev: 1, cv: 'cv1', id: id, v: diff( {}, data )}; return new Promise( ( resolve ) => { bucket.on( 'update', () => { @@ -268,7 +268,7 @@ describe( 'Channel', function() { bucket.update( key, {title: 'hello world'}, function() { channel.handleMessage( 'c:' + JSON.stringify( [{ - o: '-', ev: 1, cv: 'cv1', id: key + o: '-', ev: 1, cv: 'cv1', id: key, ccids: [] }] ) ); } ); } ) ); @@ -348,7 +348,7 @@ describe( 'Channel', function() { // We receive a remote change from "Hello world" to "Hello kansas" channel.handleMessage( 'c:' + JSON.stringify( [{ - o: 'M', ev: 2, sv: 1, cv: 'cv1', id: key, v: remoteDiff + o: 'M', ev: 2, sv: 1, cv: 'cv1', id: key, v: remoteDiff, ccids: [] }] ) ); // We're changing "Hello world" to "Goodbye world" @@ -529,7 +529,7 @@ describe( 'Channel', function() { } ); it( 'should request entire object when source version is out of date', ( done ) => { - var change = {o: 'M', id: 'thing', sv: 1, ev: 2, ccid: 'abc', cv: 'new-cv', v: diff( { hello: 'mundo'}, {hello: 'world'} ) }; + var change = {o: 'M', id: 'thing', sv: 1, ev: 2, ccids: ['abc'], cv: 'new-cv', v: diff( { hello: 'mundo'}, {hello: 'world'} ) }; channel.once( 'send', ( data ) => { equal( data, `e:${change.id}.${change.sv}` ); channel.once( 'change-version', ( cv ) => { From bc0498fe958731733fcb177e491b87e196722c0f Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Thu, 22 Feb 2018 14:07:13 -0800 Subject: [PATCH 3/8] eslint and flow errors fail tests --- .travis.yml | 5 ++--- package.json | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 72c41b1..ba10416 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: node_js sudo: false -before_install: - - npm install -g eslint@2.2 babel-eslint script: - - eslint . + - npm -s run lint + - npm -s run flow - npm test node_js: - "4.0" diff --git a/package.json b/package.json index e8ce415..2fa04da 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "flow": "flow", + "lint": "eslint .", "test": "mocha --require babel-core/register --require test/helper test/**", "prepublish": "babel -q -d lib/ src/" }, From b7a638d0ee8798822b4139dd0d2769aa045ac60d Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Thu, 22 Feb 2018 14:09:25 -0800 Subject: [PATCH 4/8] Removes outdated TODO statements --- src/simperium/channel.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/simperium/channel.js b/src/simperium/channel.js index 0b891c7..2138d19 100644 --- a/src/simperium/channel.js +++ b/src/simperium/channel.js @@ -247,13 +247,6 @@ internal.applyChange = function( change: NetworkChange, ghost: Ghost ) { let original, patch, modified; - // attempt to apply the change - // TODO: Handle errors as specified in - // 0:c:[{"ccids": ["0435edf4-3f07-4cc6-bf86-f68e6db8779c"], "id": "9e9a9616-8174-42 - // { ccids: [ '0435edf4-3f07-4cc6-bf86-f68e6db8779c' ], - // id: '9e9a9616-8174-425a-a1b0-9ed5410f1edc', - // clientid: 'node-b9776e96-c068-42ae-893a-03f50833bddb', - // error: 400 } if ( change.o === operation.MODIFY ) { const matchesStartingVersion = change.sv === ghost.version || @@ -689,14 +682,13 @@ Channel.prototype.sendChangeVersionRequest = function( cv ) { this.send( format( 'cv:%s', cv ) ); }; - type ChangeMessage = { clientid?: string, ccids?: string[], id?: string, // Bucket object being changed - o?: string, // TODO: force into valid operation types, - v?: {}, // TODO: force into valid change value - cv?: string, // Bucket cv this change represents + o?: string, + v?: {}, + cv?: string, sv?: number, ev?: number, error?: number, @@ -738,9 +730,6 @@ Channel.prototype.onChanges = function( data ) { onChange = internal.changeObject.bind( this ); changes.forEach( ( change ) => { - // TODO: parsed change _could_ be an error response - // error change shape an non-error change shape are _not_ the same - // do validation here and choose appropriate code path if ( change.error ) { applyChangeError( this, asNetworkErrorResponse( change ) ); } else { @@ -972,7 +961,7 @@ LocalQueue.prototype.resendSentChanges = function() { * * @type {Map} stores specific revisions as a cache */ -export const revisionCache: Map = new Map(); +export const revisionCache: Map = new Map(); /** * Attempts to fetch an entity's revisions From a7d22957ac86884c27082f7078e8ac208a95c83b Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Wed, 28 Feb 2018 13:22:00 -0800 Subject: [PATCH 5/8] Adding types and fixing type errors --- src/simperium/channel.js | 269 +++++++++++++-------- src/simperium/jsondiff/diff_match_patch.js | 19 +- src/simperium/jsondiff/index.js | 6 +- src/simperium/jsondiff/jsondiff.js | 64 +++-- src/simperium/util/change.js | 76 ++++-- 5 files changed, 269 insertions(+), 165 deletions(-) diff --git a/src/simperium/channel.js b/src/simperium/channel.js index 2138d19..ee248a2 100644 --- a/src/simperium/channel.js +++ b/src/simperium/channel.js @@ -3,8 +3,10 @@ import { format, inherits } from 'util' import events from 'events' import { parseMessage, parseVersionMessage, change as change_util } from './util' +import type { ObjectOperationSet } from './jsondiff'; +import type { BucketChangeType } from './util/change'; import JSONDiff from './jsondiff' -import { v4 as uuid } from 'uuid' +import uuid from 'uuid/v4' const { EventEmitter } = events; @@ -15,16 +17,25 @@ type LocalChange = { ccid: string } -type NetworkChange = { +type NetworkRemoveOperation = { + o: '-', + id: string, + ccids: string[], + cv: string +} + +type NetworkModifyOperation = { + o: 'M', id: string, ccids: string[], - o: '+' | '-' | 'M', - sv: number, - ev: number, cv: string, - v?: {} + ev: number, + sv: number, + v: ObjectOperationSet } +type NetworkChange = NetworkModifyOperation | NetworkRemoveOperation; + type NetworkChangeErrorResponse = { error: number, id: string, @@ -58,11 +69,6 @@ const CODE_EMPTY_RESPONSE = 412; const CODE_INVALID_DIFF = 440; const CODE_DUPLICATE_CHANGE = 409; -const operation = { - MODIFY: 'M', - REMOVE: '-' -}; - // internal methods used as instance methods on a Channel instance const internal = {}; @@ -72,8 +78,9 @@ const internal = {}; * @param {String} cv - the change version synced * @returns {Promise} the saved `cv` */ -internal.updateChangeVersion = function( cv ) { - return this.store.setChangeVersion( cv ).then( () => { +internal.updateChangeVersion = function( cv ): Promise { + const store: GhostStore = this.store; + return store.setChangeVersion( cv ).then( () => { // A unit test currently relies on this event, otherwise we can remove it this.emit( 'change-version', cv ); return cv; @@ -88,11 +95,15 @@ internal.updateChangeVersion = function( cv ) { * @param {Object} change - the change to apply to the object */ internal.changeObject = function( id: string, change: NetworkChange ) { - // pull out the object from the store and apply the change delta - var applyChange = internal.performChange.bind( this, change ); - - this.networkQueue.queueFor( id ).add( function( done ) { - return applyChange().then( done, done ); + // Add types for now until function is changed to accept a Channel + const channel: Channel = this; + const store: GhostStore = channel.store; + const queue: NetworkQueue = channel.networkQueue; + + queue.queueFor( id ).add( ( done ) => { + store.get( change.id ) + .then( ghost => internal.applyChange.call( channel, change, ghost ) ) + .then( done, done ); } ); }; @@ -106,12 +117,11 @@ internal.changeObject = function( id: string, change: NetworkChange ) { * @param {Object} object - object literal of the data that the change should produce * @param {Object} ghost - the ghost version used to produce the change object */ -internal.buildModifyChange = function( id, object, ghost ) { - var payload = change_util.buildChange( change_util.type.MODIFY, id, object, ghost ), - empty = true, - key; +internal.buildModifyChange = function( id: string, object: {}, ghost: Ghost ) { + const payload = change_util.buildChange( change_util.type.MODIFY, id, object, ghost ); + let empty = true; - for ( key in payload.v ) { + for ( let key in payload.v ) { if ( key ) { empty = false; break; @@ -135,9 +145,10 @@ internal.buildModifyChange = function( id, object, ghost ) { * @param {String} id - object to remove * @param {Object} ghost - current ghost object for the given id */ -internal.buildRemoveChange = function( id, ghost ) { - var payload = change_util.buildChange( change_util.type.REMOVE, id, {}, ghost ); - this.localQueue.queue( payload ); +internal.buildRemoveChange = function( id: string, ghost: Ghost ) { + const payload = change_util.buildChange( '-', id, {}, ghost ); + const localQueue: LocalQueue = this.localQueue; + localQueue.queue( payload ); }; internal.diffAndSend = function( id, object ) { @@ -152,7 +163,7 @@ internal.removeAndSend = function( id ) { // We've receive a full object from the network. Update the local instance and // notify of the new object version -internal.updateObjectVersion = function( id, version, data, original, patch, acknowledged ) { +internal.updateObjectVersion = function( id: string, version: number, data: {}, original, patch, acknowledged ): Promise<*> { var notify, changes, change, @@ -188,7 +199,7 @@ internal.updateObjectVersion = function( id, version, data, original, patch, ack return this.store.put( id, version, data ).then( notify ); }; -internal.removeObject = function( id, acknowledged ) { +internal.removeObject = function( id, acknowledged ): Promise<*> { var notify; if ( !acknowledged ) { notify = this.emit.bind( this, 'remove', id ); @@ -196,7 +207,8 @@ internal.removeObject = function( id, acknowledged ) { notify = internal.updateAcknowledged.bind( this, acknowledged ); } - return this.store.remove( id ).then( notify ); + const store: GhostStore = this.store; + return store.remove( id ).then( notify ); }; internal.updateAcknowledged = function( change: LocalChange ) { @@ -207,13 +219,8 @@ internal.updateAcknowledged = function( change: LocalChange ) { } }; -internal.performChange = function( change ) { - var success = internal.applyChange.bind( this, change ); - return this.store.get( change.id ).then( success ); -}; - -internal.findAcknowledgedChange = function( change: { id: string, ccids: string[] } ) { - var possibleChange = this.localQueue.sent[change.id]; +internal.findAcknowledgedChange = function( change: { id: string, ccids: string[] } ): ?LocalChange { + const possibleChange: ?LocalChange = this.localQueue.sent[change.id]; if ( possibleChange ) { if ( ( change.ccids || [] ).indexOf( possibleChange.ccid ) > -1 ) { return possibleChange; @@ -221,7 +228,7 @@ internal.findAcknowledgedChange = function( change: { id: string, ccids: string[ } }; -internal.requestObjectVersion = function( id, version ) { +internal.requestObjectVersion = function( id: string, version: number ) { return new Promise( resolve => { this.once( `version.${ id }.${ version }`, data => { resolve( data ); @@ -232,7 +239,8 @@ internal.requestObjectVersion = function( id, version ) { const applyChangeError = ( channel: Channel, changeError: NetworkChangeErrorResponse ) => { // run on network queue for the relevant bucket object - channel.networkQueue.queueFor( changeError.id ).add( ( done ) => { + const networkQueue: NetworkQueue = channel.networkQueue; + networkQueue.queueFor( changeError.id ).add( ( done ) => { const localChange = internal.findAcknowledgedChange.call( channel, changeError ); const error = new ChangeError( changeError, localChange ); internal.handleChangeError.call( channel, error, changeError, localChange ); @@ -240,7 +248,7 @@ const applyChangeError = ( channel: Channel, changeError: NetworkChangeErrorResp } ) }; -internal.applyChange = function( change: NetworkChange, ghost: Ghost ) { +internal.applyChange = function( change: NetworkChange, ghost: Ghost ): Promise { const acknowledged = internal.findAcknowledgedChange.call( this, change ), updateChangeVersion = internal.updateChangeVersion.bind( this, change.cv ); @@ -248,14 +256,19 @@ internal.applyChange = function( change: NetworkChange, ghost: Ghost ) { patch, modified; - if ( change.o === operation.MODIFY ) { + if ( change.o === '-' ) { + return internal.removeObject.call( this, change.id, acknowledged ).then( updateChangeVersion ); + } + + if ( change.o === 'M' ) { + const modifyChange: NetworkModifyOperation = change; const matchesStartingVersion = change.sv === ghost.version || ( change.sv === 0 && ( ghost.version === null || ghost.version === undefined ) ); if ( ! matchesStartingVersion ) { internal.requestObjectVersion.call( this, change.id, change.sv ).then( data => { - internal.applyChange.call( this, change, { key: ghost.key, version: change.sv, data } ) + internal.applyChange.call( this, change, { key: ghost.key, version: modifyChange.sv, data } ) } ); - return; + return Promise.resolve(); } original = ghost.data; @@ -263,9 +276,10 @@ internal.applyChange = function( change: NetworkChange, ghost: Ghost ) { modified = jsondiff.apply_object_diff( original, patch ); return internal.updateObjectVersion.call( this, change.id, change.ev, modified, original, patch, acknowledged ) .then( updateChangeVersion ); - } else if ( change.o === operation.REMOVE ) { - return internal.removeObject.bind( this )( change.id, acknowledged ).then( updateChangeVersion ); } + // Only changes of REMOVE and MODIFY are possible + // Should changes of ADD throw an error? + return Promise.resolve(); } internal.handleChangeError = function( err: ChangeError, change: NetworkChangeErrorResponse, acknowledged: ?LocalChange ) { @@ -340,58 +354,65 @@ internal.indexingComplete = function() { * @interface GhostStore */ interface GhostStore { -/** - * Retrieve a Ghost for the given bucket object id - * - * @function - * @name GhostStore#get - * @param {String} id - bucket object id - * @returns {Promise} - the ghost for this object - */ + /** + * Retrieve a Ghost for the given bucket object id + * + * @function + * @name GhostStore#get + * @param {String} id - bucket object id + * @returns {Promise} - the ghost for this object + */ + get( id: string ): Promise; -/** - * Save a ghost in the store. - * - * @function - * @name GhostStore#put - * @param {String} id - bucket object id - * @param {Number} version - version of ghost data - * @param {Object} data - object literal to save as this ghost's data for this version - * @returns {Promise} - the ghost for this object - */ + /** + * Save a ghost in the store. + * + * @function + * @name GhostStore#put + * @param {String} id - bucket object id + * @param {Number} version - version of ghost data + * @param {Object} data - object literal to save as this ghost's data for this version + * @returns {Promise} - the ghost for this object + */ + put(id: string, version: number, data: {}): Promise; -/** - * Delete a Ghost from the store. - * - * @function - * @name GhostStore#remove - * @param {String} id - bucket object id - * @returns {Promise} - the ghost for this object - */ + /** + * Delete a Ghost from the store. + * + * @function + * @name GhostStore#remove + * @param {String} id - bucket object id + * @returns {Promise} - the ghost for this object + */ + remove(id: string): Promise; -/** - * Iterate over existing Ghost objects with the given callback. - * - * @function - * @name GhostStore#eachGhost - * @param {ghostIterator} - function to run against each ghost - */ + /** + * Iterate over existing Ghost objects with the given callback. + * + * @function + * @name GhostStore#eachGhost + * @param {ghostIterator} - function to run against each ghost + */ + eachGhost(iterator: (Ghost) => void): void; -/** - * Get the current change version (cv) that this channel has synced. - * - * @function - * @name GhostStore#getChangeVersion - * @returns {Promise} - the current change version for the bucket - */ + /** + * Get the current change version (cv) that this channel has synced. + * + * @function + * @name GhostStore#getChangeVersion + * @returns {Promise} - the current change version for the bucket + */ + getChangeVersion(): Promise; -/** - * Set the current change version. - * - * @function - * @name GhostStore#setChangeVersion - * @returns {Promise} - resolves once the change version is saved - */ + /** + * Set the current change version. + * + * @function + * @name GhostStore#setChangeVersion + * @param {string} changeVersion - new change version + * @returns {Promise} - resolves once the change version is saved + */ + setChangeVersion(changeVersion: string): Promise; } /** @@ -686,7 +707,7 @@ type ChangeMessage = { clientid?: string, ccids?: string[], id?: string, // Bucket object being changed - o?: string, + o?: BucketChangeType, v?: {}, cv?: string, sv?: number, @@ -696,11 +717,11 @@ type ChangeMessage = { }; const requireProp = ( key: string, object: {} ): T => { - const value = object[ key ]; + const value: T = object[ key ]; if ( value ) { return value; } - throw new Error( `unexpected value ${ value } for key ${ key } in ${ JSON.stringify( object ) }` ); + throw new Error( `unexpected value for key ${ key } in ${ JSON.stringify( object ) }` ); } const asNetworkErrorResponse = ( changeMessage: ChangeMessage ): NetworkChangeErrorResponse => { @@ -712,17 +733,53 @@ const asNetworkErrorResponse = ( changeMessage: ChangeMessage ): NetworkChangeEr }; } +class ProtocolError extends Error { +} + const asNetworkChange = ( changeMessage: ChangeMessage ): NetworkChange => { - return { - id: requireProp( 'id', changeMessage ), - ccids: requireProp( 'ccids', changeMessage ), - o: requireProp( 'o', changeMessage ), - d: changeMessage.id, - cv: requireProp( 'cv', changeMessage ), - ev: requireProp( 'ev', changeMessage ), - sv: changeMessage.sv ? changeMessage.sv : 0, - v: changeMessage.v // v not required for `-` change types - }; + const operation: ?BucketChangeType = changeMessage.o; + + if ( ! changeMessage.ccids ) { + throw new ProtocolError( 'nework change missing ccids' ); + } + + if ( ! changeMessage.cv ) { + throw new ProtocolError( 'netwock change missing change version (cv)'); + } + + if ( ! changeMessage.id ) { + throw new ProtocolError( 'network change missing id'); + } + + if ( operation === '-' ) { + return { + id: changeMessage.id, + cv: changeMessage.cv, + ccids: changeMessage.ccids, + o: '-', + sv: changeMessage.sv ? changeMessage.sv : 0, + } + } + if ( operation === 'M' ) { + if ( ! changeMessage.ev ) { + throw new ProtocolError( 'network modify change missing ev' ); + } + + if ( ! changeMessage.v ) { + throw new ProtocolError( 'network modify change missing v' ); + } + + return { + id: changeMessage.id, + cv: changeMessage.cv, + ccids: changeMessage.ccids, + o: 'M', + sv: changeMessage.sv ? changeMessage.sv : 0, + ev: changeMessage.ev, + v: changeMessage.v + } + } + throw new Error( `Invalid change type ${ operation ? operation : '' } in c:${ JSON.stringify( changeMessage )}` ); } Channel.prototype.onChanges = function( data ) { @@ -787,7 +844,7 @@ function Queue() { inherits( Queue, EventEmitter ); // Add a function at the end of the queue -Queue.prototype.add = function( fn ) { +Queue.prototype.add = function( fn: ( () => void ) => void ): Queue { this.queue.push( fn ); this.start(); return this; @@ -814,7 +871,7 @@ Queue.prototype.run = function() { fn( this.run.bind( this ) ); } -function LocalQueue( store ) { +function LocalQueue( store: GhostStore ) { this.store = store; this.sent = {}; this.queues = {}; diff --git a/src/simperium/jsondiff/diff_match_patch.js b/src/simperium/jsondiff/diff_match_patch.js index 6f97a7a..9bdccea 100644 --- a/src/simperium/jsondiff/diff_match_patch.js +++ b/src/simperium/jsondiff/diff_match_patch.js @@ -1,4 +1,3 @@ -module.exports = diff_match_patch; /** * Diff Match and Patch @@ -29,7 +28,7 @@ module.exports = diff_match_patch; * Class containing the diff, match and patch methods. * @constructor */ -function diff_match_patch() { +export default function diff_match_patch() { // Defaults. // Redefine these in your program to override the defaults. @@ -65,9 +64,9 @@ function diff_match_patch() { * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] * which means: delete 'Hello', add 'Goodbye' and keep ' world.' */ -var DIFF_DELETE = -1; -var DIFF_INSERT = 1; -var DIFF_EQUAL = 0; +export const DIFF_DELETE = -1; +export const DIFF_INSERT = 1; +export const DIFF_EQUAL = 0; /** @typedef {{0: number, 1: string}} */ diff_match_patch.Diff; @@ -2183,13 +2182,3 @@ diff_match_patch.patch_obj.prototype.toString = function() { } return text.join('').replace(/%20/g, ' '); }; - - -// Export these global variables so that they survive Google's JS compiler. -// In a browser, 'this' will be 'window'. -// Users of node.js should 'require' the uncompressed version since Google's -// JS compiler may break the following exports for non-browser environments. -module.exports['diff_match_patch'] = diff_match_patch; -module.exports['DIFF_DELETE'] = DIFF_DELETE; -module.exports['DIFF_INSERT'] = DIFF_INSERT; -module.exports['DIFF_EQUAL'] = DIFF_EQUAL; \ No newline at end of file diff --git a/src/simperium/jsondiff/index.js b/src/simperium/jsondiff/index.js index 568cfa5..85344f8 100644 --- a/src/simperium/jsondiff/index.js +++ b/src/simperium/jsondiff/index.js @@ -1,8 +1,12 @@ +// @flow import JSONDiff from './jsondiff' import diff_match_patch from './diff_match_patch' +import type { ObjectOperationSet } from './jsondiff'; + +export type { ObjectOperationSet }; export { JSONDiff as jsondiff, diff_match_patch } -export default function init( options ) { +export default function init( options: ?{ list_diff: boolean } ): JSONDiff { return new JSONDiff( options ); } diff --git a/src/simperium/jsondiff/jsondiff.js b/src/simperium/jsondiff/jsondiff.js index 8ff9f16..579fd06 100644 --- a/src/simperium/jsondiff/jsondiff.js +++ b/src/simperium/jsondiff/jsondiff.js @@ -1,14 +1,46 @@ -var diff_match_patch = require('./diff_match_patch'); +// @flow +import diff_match_patch, { DIFF_EQUAL, DIFF_INSERT, DIFF_DELETE } from './diff_match_patch'; -// stolen from https://raw.github.com/Simperium/jsondiff/master/src/jsondiff.js -(function() { - var jsondiff, - __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - __hasProp = Object.prototype.hasOwnProperty; +// These ar the types of changes that can be applied to a value +type PropertyChangeType + = '+' // add the value at `v` + | '-' // remove the key from the object + | 'r' // replace the key value of object with `v` + | 'I' // increment the value at key, requires the value to be numerical + | 'L' // apply a list diff operation to the property at v + | 'O' // apply an object diff operation to the property at v + | 'd' // apply a diff match patch operation to the property at v + +// Add a value, will exist within an object operation +type AddOperation = { o: '+', v: any } +type ReplaceOperation = { o: 'r', v: any } + +type RemoveOperation = { o: '-' }; +type IncrementOperation = { o: 'I', v: number } +type ListOperation = { o: 'L', v: { [key: number]: Operation } }; +// an object diff is indexed by the key name that the diff applies to + +export type ObjectOperationSet = { [key: string]: Operation }; +type ObjectOperation = { o: 'O', v: ObjectOperationSet }; - jsondiff = (function() { +type DiffMatchPathOperation = { o: 'd', v: string } - function jsondiff(options) { + +export type Operation + // adds a value to an object, is a child of another change + = AddOperation + | ReplaceOperation + | RemoveOperation + | IncrementOperation + | ListOperation + | ObjectOperation + | DiffMatchPathOperation + +// stolen from https://raw.github.com/Simperium/jsondiff/master/src/jsondiff.js + const __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + const __hasProp = Object.prototype.hasOwnProperty; + + export default function jsondiff(options: ? { list_diff: boolean } ) { this.options = options || {list_diff: true}; this.patch_apply_with_offsets = __bind(this.patch_apply_with_offsets, this); this.transform_object_diff = __bind(this.transform_object_diff, this); @@ -168,7 +200,7 @@ var diff_match_patch = require('./diff_match_patch'); return diffs; }; - jsondiff.prototype.object_diff = function(a, b) { + jsondiff.prototype.object_diff = function(a: {}, b: {}): DiffSet { var diffs, key; diffs = {}; if (!(a != null) || !(b != null)) return {}; @@ -298,8 +330,8 @@ var diff_match_patch = require('./diff_match_patch'); return patched; }; - jsondiff.prototype.apply_object_diff = function(s, diffs) { - var dmp_diffs, dmp_patches, dmp_result, key, op, patched; + jsondiff.prototype.apply_object_diff = function(s: {}, diffs: ObjectOperationSet): {} { + var dmp_diffs, dmp_patches, dmp_result, key, op, patched: {}; patched = this.deepCopy(s); for (key in diffs) { if (!__hasProp.call(diffs, key)) continue; @@ -429,7 +461,7 @@ var diff_match_patch = require('./diff_match_patch'); return ad_new; }; - jsondiff.prototype.transform_object_diff = function(ad, bd, s) { + jsondiff.prototype.transform_object_diff = function(ad: ObjectOperationSet, bd: ObjectOperationSet, s: {}): ?ObjectOperationSet { var a_patches, ab_text, ad_new, aop, b_patches, b_text, bop, dmp_diffs, dmp_patches, dmp_result, key, sk, _ref; ad_new = this.deepCopy(ad); for (key in ad) { @@ -611,11 +643,3 @@ var diff_match_patch = require('./diff_match_patch'); text = text.substring(nullPadding.length, text.length - nullPadding.length); return text; }; - - return jsondiff; - - })(); - - module.exports = jsondiff; - -}).call(); \ No newline at end of file diff --git a/src/simperium/util/change.js b/src/simperium/util/change.js index 96c8a72..943c080 100644 --- a/src/simperium/util/change.js +++ b/src/simperium/util/change.js @@ -1,10 +1,13 @@ -import { v4 as uuid } from 'uuid' +// @flow +import uuid from 'uuid/v4' import jsondiff from '../jsondiff' +import type { ObjectOperationSet } from '../jsondiff'; -const changeTypes = { +export type BucketChangeType = 'M' | '-'; + +const changeTypes: { [name: string]: BucketChangeType } = { MODIFY: 'M', - REMOVE: '-', - ADD: '+' + REMOVE: '-' }; const { object_diff, transform_object_diff, apply_object_diff } = jsondiff( {list_diff: false} ) @@ -17,32 +20,55 @@ export { apply_diff as apply } -function modify( id, version, patch ) { - return { o: changeTypes.MODIFY, id: id, ccid: uuid.v4(), v: patch }; +function modify( id: string, version: number, patch: {} ) { + return { o: changeTypes.MODIFY, id: id, ccid: uuid(), v: patch }; } -function buildChange( type, id, object, ghost ) { +function buildChange( type: BucketChangeType, id: string, object: {}, ghost: {| version: number, data: {} |} ) { return buildChangeFromOrigin( type, id, ghost.version, object, ghost.data ); } -function buildChangeFromOrigin( type, id, version, target, origin ) { - var changeData = { - o: type, - id: id, - ccid: uuid.v4() - }; +export type ModifyChange = { + o: 'M', + id: string, + ccid: string, + v: ObjectOperationSet, + sv?: number +} - // Remove operations have no source version or diff - if ( type === changeTypes.REMOVE ) return changeData; +export type RemoveChange = { + o: '-', + id: string, + ccid: string +} - if ( version > 0 ) changeData.sv = version; +export type Change = ModifyChange | RemoveChange; - changeData.v = object_diff( origin, target ); +function buildChangeFromOrigin( type: BucketChangeType, id: string, version: number, target: {}, origin: {} ): Change { + + if ( type === changeTypes.REMOVE ) { + return { + o: '-', + id, + ccid: uuid() + }; + } - return changeData; + + const change: ModifyChange = { + o: 'M', + id, + ccid: uuid(), + v: object_diff( origin, target ) + } + + if ( version > 0 ) { + change.sv = version; + } + return change; } -function compressChanges( changes, origin ) { +function compressChanges( changes: Array, origin: {} ): ?ObjectOperationSet { var modified; if ( changes.length === 0 ) { @@ -50,13 +76,17 @@ function compressChanges( changes, origin ) { } if ( changes.length === 1 ) { - return changes[0].v; + const change: Change = changes[0]; + if ( change.o === 'M' ) { + return change.v; + } + return null; } modified = changes.reduce( function( from, change ) { // deletes when, any changes after a delete are ignored if ( from === null ) return null; - if ( from.o === changeTypes.REMOVE ) return null; + if ( change.o === '-' ) return null; return apply_object_diff( from, change.v ); }, origin ); @@ -65,10 +95,10 @@ function compressChanges( changes, origin ) { return object_diff( origin, modified ); } -function rebase( local_diff, remote_diff, origin ) { +function rebase( local_diff: ObjectOperationSet, remote_diff: ObjectOperationSet, origin: {} ) { return transform_object_diff( local_diff, remote_diff, origin ); } -function apply_diff( patch, object ) { +function apply_diff( patch: Change, object: {} ) { return apply_object_diff( object, patch ); } From edc3d322ab87c64b5eec500daa8d2e9ed9abdac6 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Wed, 28 Feb 2018 13:46:14 -0800 Subject: [PATCH 6/8] Prevent flow from failing builds --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c514343..4503a2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,12 @@ language: node_js sudo: false script: +# Enable flow to have flow errors fail the build +# - npm -s run flow # lint errors fail the build - npm -s run lint # unit tests with coverage report - npm -s run test:coverage -- npm -s run flow node_js: - '8' - '7' From fb3cb6f5ec5643a905f2805ac7e432e2d51039a7 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Wed, 28 Feb 2018 13:46:48 -0800 Subject: [PATCH 7/8] Moves definitions to fix lint errors - fixes some warnings --- src/simperium/channel.js | 192 +++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/src/simperium/channel.js b/src/simperium/channel.js index ee248a2..0a8c186 100644 --- a/src/simperium/channel.js +++ b/src/simperium/channel.js @@ -10,6 +10,100 @@ import uuid from 'uuid/v4' const { EventEmitter } = events; +type Ghost = { + key: string, + version: number, + data: {} +} + +/** + * A ghost represents a version of a bucket object as known by Simperium + * + * Generally a client will keep the last known ghost stored locally for efficient + * diffing and patching of Simperium change operations. + * + * @typedef {Object} Ghost + * @property {Number} version - the ghost's version + * @property {String} key - the simperium bucket object id this ghost is for + * @property {Object} data - the data for the given ghost version + */ + +/** + * Callback function used by the ghost store to iterate over existing ghosts + * + * @callback ghostIterator + * @param {Ghost} - the current ghost + */ + +/** + * A GhostStore provides the store mechanism for ghost data that the Channel + * uses to maintain syncing state and producing change operations for + * Bucket objects. + * + * @interface GhostStore + */ +interface GhostStore { + /** + * Retrieve a Ghost for the given bucket object id + * + * @function + * @name GhostStore#get + * @param {String} id - bucket object id + * @returns {Promise} - the ghost for this object + */ + get( id: string ): Promise; + + /** + * Save a ghost in the store. + * + * @function + * @name GhostStore#put + * @param {String} id - bucket object id + * @param {Number} version - version of ghost data + * @param {Object} data - object literal to save as this ghost's data for this version + * @returns {Promise} - the ghost for this object + */ + put( id: string, version: number, data: {} ): Promise; + + /** + * Delete a Ghost from the store. + * + * @function + * @name GhostStore#remove + * @param {String} id - bucket object id + * @returns {Promise} - the ghost for this object + */ + remove( id: string ): Promise; + + /** + * Iterate over existing Ghost objects with the given callback. + * + * @function + * @name GhostStore#eachGhost + * @param {ghostIterator} - function to run against each ghost + */ + eachGhost( iterator: ( Ghost ) => void ): void; + + /** + * Get the current change version (cv) that this channel has synced. + * + * @function + * @name GhostStore#getChangeVersion + * @returns {Promise} - the current change version for the bucket + */ + getChangeVersion(): Promise; + + /** + * Set the current change version. + * + * @function + * @name GhostStore#setChangeVersion + * @param {string} changeVersion - new change version + * @returns {Promise} - resolves once the change version is saved + */ + setChangeVersion( changeVersion: string ): Promise; +} + const jsondiff = new JSONDiff( {list_diff: false} ); type LocalChange = { @@ -44,12 +138,6 @@ type NetworkChangeErrorResponse = { hasSentFullObject?: boolean }; -type Ghost = { - key: string, - version: number, - data: {} -} - class ChangeError extends Error { code: number; changeError: NetworkChangeErrorResponse; @@ -327,94 +415,6 @@ internal.indexingComplete = function() { this.emit( 'ready' ) } -/** - * A ghost represents a version of a bucket object as known by Simperium - * - * Generally a client will keep the last known ghost stored locally for efficient - * diffing and patching of Simperium change operations. - * - * @typedef {Object} Ghost - * @property {Number} version - the ghost's version - * @property {String} key - the simperium bucket object id this ghost is for - * @property {Object} data - the data for the given ghost version - */ - -/** - * Callback function used by the ghost store to iterate over existing ghosts - * - * @callback ghostIterator - * @param {Ghost} - the current ghost - */ - -/** - * A GhostStore provides the store mechanism for ghost data that the Channel - * uses to maintain syncing state and producing change operations for - * Bucket objects. - * - * @interface GhostStore - */ -interface GhostStore { - /** - * Retrieve a Ghost for the given bucket object id - * - * @function - * @name GhostStore#get - * @param {String} id - bucket object id - * @returns {Promise} - the ghost for this object - */ - get( id: string ): Promise; - - /** - * Save a ghost in the store. - * - * @function - * @name GhostStore#put - * @param {String} id - bucket object id - * @param {Number} version - version of ghost data - * @param {Object} data - object literal to save as this ghost's data for this version - * @returns {Promise} - the ghost for this object - */ - put(id: string, version: number, data: {}): Promise; - - /** - * Delete a Ghost from the store. - * - * @function - * @name GhostStore#remove - * @param {String} id - bucket object id - * @returns {Promise} - the ghost for this object - */ - remove(id: string): Promise; - - /** - * Iterate over existing Ghost objects with the given callback. - * - * @function - * @name GhostStore#eachGhost - * @param {ghostIterator} - function to run against each ghost - */ - eachGhost(iterator: (Ghost) => void): void; - - /** - * Get the current change version (cv) that this channel has synced. - * - * @function - * @name GhostStore#getChangeVersion - * @returns {Promise} - the current change version for the bucket - */ - getChangeVersion(): Promise; - - /** - * Set the current change version. - * - * @function - * @name GhostStore#setChangeVersion - * @param {string} changeVersion - new change version - * @returns {Promise} - resolves once the change version is saved - */ - setChangeVersion(changeVersion: string): Promise; -} - /** * Maintains syncing state for a Simperium bucket. * @@ -744,11 +744,11 @@ const asNetworkChange = ( changeMessage: ChangeMessage ): NetworkChange => { } if ( ! changeMessage.cv ) { - throw new ProtocolError( 'netwock change missing change version (cv)'); + throw new ProtocolError( 'netwock change missing change version (cv)' ); } if ( ! changeMessage.id ) { - throw new ProtocolError( 'network change missing id'); + throw new ProtocolError( 'network change missing id' ); } if ( operation === '-' ) { From f462d8ffd7c3c492ec6ea1c675de227cf63fce5d Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Wed, 28 Feb 2018 13:50:09 -0800 Subject: [PATCH 8/8] Moves type definiton below relevant comment --- src/simperium/channel.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/simperium/channel.js b/src/simperium/channel.js index 0a8c186..e4aa1e4 100644 --- a/src/simperium/channel.js +++ b/src/simperium/channel.js @@ -10,12 +10,6 @@ import uuid from 'uuid/v4' const { EventEmitter } = events; -type Ghost = { - key: string, - version: number, - data: {} -} - /** * A ghost represents a version of a bucket object as known by Simperium * @@ -27,6 +21,11 @@ type Ghost = { * @property {String} key - the simperium bucket object id this ghost is for * @property {Object} data - the data for the given ghost version */ +type Ghost = { + key: string, + version: number, + data: {} +} /** * Callback function used by the ghost store to iterate over existing ghosts