Skip to content

Commit 514f996

Browse files
committed
Merge remote-tracking branch 'origin/master' into cleanup/auth
2 parents 4ad56fb + 54a44c4 commit 514f996

File tree

14 files changed

+840
-322
lines changed

14 files changed

+840
-322
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
},
1010
"scripts": {
1111
"flow": "flow",
12-
"test": "mocha --compilers js:babel-core/register test/**",
12+
"test": "mocha --compilers js:babel-core/register --require test/helper test/**",
1313
"prepublish": "babel -q -d lib/ src/"
1414
},
1515
"author": "",
1616
"license": "BSD-2-Clause",
1717
"dependencies": {
18-
"node-uuid": "^1.4.7",
18+
"uuid": "^3.2.1",
1919
"websocket": "^1.0.22"
2020
},
2121
"devDependencies": {

src/simperium/bucket.js

Lines changed: 297 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,341 @@
11
import { EventEmitter } from 'events'
22
import { inherits } from 'util'
3-
import uuid from 'node-uuid';
3+
import { v4 as uuid } from 'uuid';
44

5-
export default function Bucket( name, storeProvider ) {
5+
/**
6+
* @callback taskCallback
7+
* @param {?Error} - if an error occurred it will be provided, otherwise null
8+
* @param {Any} - the result of task
9+
*/
10+
11+
/**
12+
* Convenience function to turn a function that uses a callback into a function
13+
* that returns a Promise.
14+
*
15+
* @param {taskCallback} task - function that expects a single callback argument
16+
* @returns {Promise} callback wrapped in a promise interface
17+
*/
18+
const callbackAsPromise = ( task ) => new Promise( ( resolve, reject ) => {
19+
task( ( error, result ) => error ? reject( error ) : resolve( result ) );
20+
} );
21+
22+
/**
23+
* Runs a promise with a callback (if one is provided) to support the old callback API.
24+
* NOTE: if the callback API is removed this is a place to warn users
25+
*
26+
* @param {Function} [callback] - if provided, will be called with the expected values
27+
* @param {Promise} promise - promise to run, executes callback if provieded
28+
* @returns {Promise} promise is passed through
29+
*/
30+
const deprecateCallback = ( callback, promise ) => {
31+
if ( typeof callback === 'function' ) {
32+
// Potentially could warn here if we decide to remove callback API
33+
return promise.then(
34+
result => {
35+
callback( null, result );
36+
return result;
37+
},
38+
error => {
39+
callback( error );
40+
return error;
41+
}
42+
);
43+
}
44+
return promise;
45+
};
46+
47+
/**
48+
* A bucket object represents the data stored in Simperium for the given id
49+
*
50+
* @typedef {Object} BucketObject
51+
* @param {String} id - bucket object id
52+
* @param {Object} data - object literal of bucket object data stored at the id
53+
* @param {?Boolean} isIndexing - used to indicate that the bucket is being indexed
54+
*/
55+
56+
/**
57+
* @callback bucketStoreGetCallback
58+
* @param {?Error}
59+
* @param {?BucketObject}
60+
*/
61+
62+
/**
63+
* @callback bucketStoreRemoveCallback
64+
* @param {?Error}
65+
*/
66+
67+
/**
68+
* @callback bucketStoreFindCallback
69+
* @param {?Error}
70+
* @param {?BucketObject[]}
71+
*/
72+
73+
/**
74+
* Used by a bucket to store bucket object data.
75+
*
76+
* @interface BucketStore
77+
*/
78+
79+
/**
80+
* Retrieve a bucket object from the store
81+
* @function
82+
* @name BucketStore#get
83+
* @param {String} id - the bucket object id to fetch
84+
* @param {bucketStoreGetCallback} - callback once the object is fetched
85+
*/
86+
87+
/**
88+
* Updates the data for the given object id.
89+
*
90+
* @function
91+
* @name BucketStore#update
92+
* @param {String} id - to of object to update
93+
* @param {Object} data - data to update the object to
94+
* @param {Boolean} isIndexing - indicates the object is being downloaded during an index
95+
* @param {bucketStoreGetCallback}
96+
*/
97+
98+
/**
99+
* Deletes the object at id from the datastore.
100+
*
101+
* @function
102+
* @name BucketStore#remove
103+
* @param {String} id - object to delete from the bucket
104+
* @param {bucketStoreRemoveCallback} - called once the object is deleted
105+
*/
106+
107+
/**
108+
* Fetchs all bucket objects from the datastore.
109+
*
110+
* @function
111+
* @name BucketStore#find
112+
* @param {?Object} query - currently undefined
113+
* @param {bucketStoreFindCallback} - called with results
114+
*/
115+
116+
/**
117+
* Turns existing bucket storage provider callback api into a promise based API
118+
*
119+
* @param {BucketStore} store - a bucket storage object
120+
* @returns {Object} store api methods that use Promises instead of callbacks
121+
*/
122+
const promiseAPI = store => ( {
123+
get: id =>
124+
callbackAsPromise( store.get.bind( store, id ) ),
125+
update: ( id, object, isIndexing ) =>
126+
callbackAsPromise( store.update.bind( store, id, object, isIndexing ) ),
127+
remove: id =>
128+
callbackAsPromise( store.remove.bind( store, id ) ),
129+
find: query =>
130+
callbackAsPromise( store.find.bind( store, query ) )
131+
} );
132+
133+
/**
134+
* A bucket that syncs data with Simperium.
135+
*
136+
* @param {String} name - Simperium bucket name
137+
* @param {bucketStoreProvider} storeProvider - a factory function that provides a bucket store
138+
* @param {Channel} channel - a channel instance used for syncing Simperium data
139+
*/
140+
export default function Bucket( name, storeProvider, channel ) {
6141
EventEmitter.call( this );
7142
this.name = name;
8143
this.store = storeProvider( this );
144+
this.storeAPI = promiseAPI( this.store );
9145
this.isIndexing = false;
146+
147+
/**
148+
* Listeners for channel events that will be added to Channel instance
149+
*/
150+
this.onChannelIndex = this.emit.bind( this, 'index' );
151+
this.onChannelError = this.emit.bind( this, 'error' );
152+
this.onChannelUpdate = ( id, data ) => {
153+
this.update( id, data, { sync: false } );
154+
};
155+
156+
this.onChannelIndexingStateChange = ( isIndexing ) => {
157+
this.isIndexing = isIndexing;
158+
if ( isIndexing ) {
159+
this.emit( 'indexing' );
160+
}
161+
};
162+
163+
this.onChannelRemove = ( id ) => this.remove( id );
164+
165+
if ( channel ) {
166+
this.setChannel( channel );
167+
}
10168
}
11169

12170
inherits( Bucket, EventEmitter );
13171

172+
/**
173+
* Sets the channel the Bucket will use to sync changes.
174+
*
175+
* This exists to allow the Client to provide a backwards compatible API. There
176+
* is probably no reason to change the Channel once it's already set.
177+
*
178+
* @param {Channel} channel - channel instance to use for syncing
179+
*/
180+
Bucket.prototype.setChannel = function( channel ) {
181+
if ( this.channel ) {
182+
this.channel
183+
.removeListener( 'index', this.onChannelIndex )
184+
.removeListener( 'error', this.onChannelError )
185+
.removeListener( 'update', this.onChannelUpdate )
186+
.removeListener( 'indexingStateChange', this.onChannelIndexingStateChange )
187+
.removeListener( 'remove', this.onChannelRemove );
188+
}
189+
this.channel = channel;
190+
channel
191+
// forward the index and error events from the channel
192+
.on( 'index', this.onChannelIndex )
193+
.on( 'error', this.onChannelError )
194+
// when the channel updates or removes data, the bucket should apply
195+
// the same updates
196+
.on( 'update', this.onChannelUpdate )
197+
.on( 'indexingStateChange', this.onChannelIndexingStateChange )
198+
.on( 'remove', this.onChannelRemove );
199+
};
200+
201+
/**
202+
* Reloads all the data from the currently cached set of ghost data
203+
*/
14204
Bucket.prototype.reload = function() {
15-
this.emit( 'reload' );
205+
this.channel.reload();
16206
};
17207

208+
/**
209+
* Stores an object in the bucket and syncs it to simperium. Generates an
210+
* object ID to represent the object in simperium.
211+
*
212+
* @param {Object} object - plain js object literal to be saved/synced
213+
* @param {?bucketStoreGetCallback} callback - runs when object has been saved
214+
* @return {Promise<Object>} data stored in the bucket
215+
*/
18216
Bucket.prototype.add = function( object, callback ) {
19-
var id = uuid.v4();
217+
var id = uuid();
20218
return this.update( id, object, callback );
21219
};
22220

221+
/**
222+
* Requests the object data stored in the bucket for the given id.
223+
*
224+
* @param {String} id - bucket object id
225+
* @param {?bucketStoreGetCallback} callback - with the data stored in the bucket
226+
* @return {Promise<Object>} the object id, data and indexing status
227+
*/
23228
Bucket.prototype.get = function( id, callback ) {
24-
return this.store.get( id, callback );
229+
return deprecateCallback( callback, this.storeAPI.get( id ) );
25230
};
26231

232+
/**
233+
* Update the bucket object of `id` with the given data.
234+
*
235+
* @param {String} id - the bucket id for the object to update
236+
* @param {Object} data - object literal to replace the object data with
237+
* @param {Object} [options] - optional settings
238+
* @param {Boolean} [options.sync=true] - false if object should not be synced with this update
239+
* @param {?bucketStoreGetCallback} callback - executed when object is updated localy
240+
* @returns {Promise<Object>} - update data
241+
*/
27242
Bucket.prototype.update = function( id, data, options, callback ) {
28243
if ( typeof options === 'function' ) {
29244
callback = options;
245+
options = { sync: true };
246+
}
247+
248+
if ( !! options === false ) {
249+
options = { sync: true };
30250
}
31-
return this.store.update( id, data, this.isIndexing, callback );
251+
252+
const task = this.storeAPI.update( id, data, this.isIndexing )
253+
.then( bucketObject => {
254+
this.emit( 'update', id, bucketObject.data );
255+
this.channel.update( bucketObject, options.sync );
256+
return bucketObject;
257+
} );
258+
return deprecateCallback( callback, task );
32259
};
33260

261+
/**
262+
* @callback bucketHasLocalChanges
263+
* @param {?Error}
264+
* @param {?Boolean}
265+
*/
266+
267+
/**
268+
* Check if the bucket has pending changes that have not yet been synced.
269+
*
270+
* @param {?bucketHasLocalChanges} callback - optional callback to receive response
271+
* @returns {Promise<Boolean>} resolves to true if their are still changes to sync
272+
*/
34273
Bucket.prototype.hasLocalChanges = function( callback ) {
35-
callback( null, false );
274+
return deprecateCallback( callback, this.channel.hasLocalChanges() );
36275
};
37276

277+
/**
278+
* @callback bucketGetVersion
279+
* @param {?Error}
280+
* @param {Number}
281+
*/
282+
283+
/**
284+
* Gets the currently synced version number for the specified object id.
285+
*
286+
* A version of `0` indicates that an object has not been added to simperium yet.
287+
*
288+
* @param {String} id - object to get the version for
289+
* @param {?bucketGetVersionCallback} callback - optional callback
290+
* @returns {Promise<number>} - resolves to the current synced version
291+
*/
38292
Bucket.prototype.getVersion = function( id, callback ) {
39-
callback( null, 0 );
293+
return deprecateCallback( callback, this.channel.getVersion( id ) );
40294
};
41295

296+
/**
297+
* Attempts to sync the object specified by `id` using whatever data
298+
* is locally stored for the object
299+
*
300+
* @param {String} id - object to sync
301+
* @param {?bucketStoreGetCallback} callback - optional callback
302+
* @returns {Promise<Object>} - object id, data
303+
*/
42304
Bucket.prototype.touch = function( id, callback ) {
43-
return this.store.get( id, ( e, object ) => {
44-
if ( e ) return callback( e );
45-
this.update( object.id, object.data, callback );
46-
} );
305+
const task = this.storeAPI.get( id )
306+
.then( object => this.update( object.id, object.data ) );
307+
308+
return deprecateCallback( callback, task );
47309
};
48310

311+
/**
312+
* Deletes the object from the bucket
313+
*
314+
* @param {String} id - object to delete
315+
* @param {?bucketStoreRemoveCallback} callback - optional callback
316+
* @returns {Promise<Void>} - resolves when object has been deleted
317+
*/
49318
Bucket.prototype.remove = function( id, callback ) {
50-
return this.store.remove( id, callback );
319+
const task = this.storeAPI.remove( id )
320+
.then( ( result ) => {
321+
this.emit( 'remove', id );
322+
this.channel.remove( id );
323+
return result;
324+
} )
325+
return deprecateCallback( callback, task );
51326
};
52327

53328
Bucket.prototype.find = function( query, callback ) {
54-
return this.store.find( query, callback );
329+
return deprecateCallback( callback, this.storeAPI.find( query ) );
55330
};
56331

332+
/**
333+
* Gets all known past versions of an object
334+
*
335+
* @param {String} id - object to fetch revisions for
336+
* @param {Function} [callback] - optional callback
337+
* @returns {Promise<Array<Object>>} - list of objects with id, data and version
338+
*/
57339
Bucket.prototype.getRevisions = function( id, callback ) {
58-
// Overridden in Channel
59-
callback( new Error( 'Failed to fetch revisions for' + id ) );
340+
return deprecateCallback( callback, this.channel.getRevisions( id ) );
60341
}

0 commit comments

Comments
 (0)