Fix issue 252 - Add reconnection option

This commit is contained in:
Ali Al Dallal 2014-09-12 19:17:51 -04:00
Родитель 2262990e13
Коммит efe6ec768c
9 изменённых файлов: 174 добавлений и 76 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -26,5 +26,6 @@ node_modules
*~
.env
client/dist
client/vendors
demo/js/compiled

Просмотреть файл

@ -104,7 +104,12 @@ module.exports = function(grunt) {
files: ['package.json', 'bower.json'],
commit: true,
commitMessage: 'v%VERSION%',
commitFiles: ['package.json', 'bower.json', './client/dist/makedrive.js', './client/dist/makedrive.min.js'],
commitFiles: [
'package.json', 'bower.json', './client/dist/makedrive.js',
'./client/dist/makedrive.min.js', './demo/js/compiled/app.min.js',
'./demo/js/compiled/app.min.map', './demo/js/compiled/dependencies.min.js',
'./demo/js/compiled/dependencies.min.map', './demo/assets/css/main.css'
],
createTag: true,
tagName: 'v%VERSION%',
tagMessage: 'v%VERSION%',
@ -185,6 +190,13 @@ module.exports = function(grunt) {
spawn: false
}
},
makeDriveClient: {
files: ['client/src/*.js'],
tasks: ["build"],
options: {
spawn: false
}
},
less: {
files: ['demo/assets/less/*'],
tasks: ['less:dist'],
@ -215,9 +227,9 @@ module.exports = function(grunt) {
grunt.registerTask( "test", [ "jshint", "exec:run_mocha" ] );
grunt.registerTask( "default", [ "test" ] );
grunt.registerTask( "init", [ "exec:grunt_bower" ] );
grunt.registerTask( "build", [ "init", "clean", "browserify:makedriveClient", "uglify" ] );
grunt.registerTask( "build", [ "clean", "browserify:makedriveClient", "uglify:develop" ] );
grunt.registerTask( "install", [ "less", "uglify:dependencies", "uglify:angular_app" ] );
grunt.registerTask( "dev", [ "less", "uglify:angular_app", "express:dev", "watch" ] );
grunt.registerTask( "dev", [ "less", "uglify:angular_app", "build", "express:dev", "watch" ] );
// Complex multi-tasks
grunt.registerTask('publish', 'Publish MakeDrive as a new version to NPM, bower and github.', function(patchLevel) {

Просмотреть файл

@ -109,6 +109,10 @@ Option | Value | Definition
------ | ----- |----------------------------------
`manual` | `true` | by default the filesystem syncs automatically in the background. This disables it.
`memory` | `<Boolean>` | by default we use a persistent store (indexeddb or websql). Using memory=true overrides and uses a temporary ram disk.
`autoReconnect` | `<Boolean>` | 'true' by default. When toggled to 'true', MakeDrive will automatically try to reconnect to the server if the WebSocket closed for any reason (e.g. no network connection or server crash).
`reconnectAttempts` | `<Number>` | By default, MakeDrive will try to reconnect forever. This sets a maximum number for attempts, after which a reconnect_failed event will be emitted.
`reconnectionDelay` | `<Number>` | Default to 1000 (ms). How long to wait before attempting a new reconnection.
`reconnectionDelayMax` | `<Number>` | Default to 5000 (ms). Maximum amount of time to wait between reconnections. Each attempt increases the reconnection by the amount specified by reconnectionDelay.
`provider` | `<Object>` | a Filer data provider to use instead of the default provider normally used. The provider given should already be instantiated (i.e., don't pass a constructor function).
`forceCreate` | `<Boolean>` | by default we return the same fs instance with every call to `MakeDrive.fs()`. In some cases it is necessary to have multiple instances. Using forceCreate=true does this.
`interval` | `<Number>` | by default, the filesystem syncs every minute if auto syncing is turned on, otherwise the interval between syncs can be specified in ms.
@ -135,6 +139,8 @@ Event | Description
----- | -------------------------------------------
`'error'`| an error occurred while connecting/syncing. The error object is passed as the first arg to the event.
`'connected'` | a connection was established with the sync server
`'reconnect_failed'` | fired when the maximum reconnect attempts is reached and a connection could not be made.
`'reconnecting'` | fired every time a reconnect attempt is made.
`'disconnected'` | the connection to the sync server was lost, either due to the client or server.
`'syncing'` | a sync with the server has begun. A subsequent `'completed'` or `'error'` event should follow at some point, indicating whether or not the sync was successful.
`'completed'` | a sync has completed and was successful.

Просмотреть файл

@ -82,7 +82,6 @@ var SyncFileSystem = require('./sync-filesystem.js');
var Filer = require('../../lib/filer.js');
var resolvePath = require('../../lib/sync-path-resolver').resolve;
var EventEmitter = require('events').EventEmitter;
var request = require('request');
var MakeDrive = {};
module.exports = MakeDrive;
@ -90,6 +89,7 @@ module.exports = MakeDrive;
function createFS(options) {
options.manual = options.manual === true;
options.memory = options.memory === true;
options.autoReconnect = options.autoReconnect !== false;
// Use a supplied provider, in memory RAM disk, or Fallback provider (default).
var provider;
@ -281,7 +281,7 @@ function createFS(options) {
// instance for all rsync operations on the filesystem, so that we
// can untangle changes done by user vs. sync code.
manager = new SyncManager(sync, _fs);
manager.init(url, token, function(err) {
manager.init(url, token, options, function(err) {
if(err) {
sync.onError(err);
return;
@ -297,40 +297,7 @@ function createFS(options) {
};
});
}
// If we were provided a token, we can connect right away, otherwise
// we need to get one first via the /api/sync route
if(token) {
connect(token);
} else {
// Remove WebSocket protocol from URL, and swap for http:// or https://
// ws://drive.webmaker.org/ -> http://drive.webmaker.org/api/sync
var apiSync = url.replace(/^([^\/]*\/\/)?/, function(match, p1) {
return p1 === 'wss://' ? 'https://' : 'http://';
});
// Also add /api/sync to the end:
apiSync = apiSync.replace(/\/?$/, '/api/sync');
request({
url: apiSync,
method: 'GET',
json: true,
withCredentials: true
}, function(err, msg, body) {
var statusCode;
var error;
statusCode = msg && msg.statusCode;
error = statusCode !== 200 ?
{ message: err || 'Unable to get token', code: statusCode } : null;
if(error) {
sync.onError(error);
} else {
connect(body);
}
});
}
connect(token);
};
// Disconnect from the server

Просмотреть файл

@ -29,7 +29,6 @@ function handleRequest(syncManager, data) {
var fs = syncManager.fs;
var sync = syncManager.sync;
var session = syncManager.session;
var socket = syncManager.socket;
function handleChecksumRequest() {
var srcList = session.srcList = data.content.srcList;
@ -46,7 +45,7 @@ function handleRequest(syncManager, data) {
var message = SyncMessage.request.diffs;
message.content = {checksums: checksums};
socket.send(message.stringify());
syncManager.send(message.stringify());
});
}
@ -60,7 +59,7 @@ function handleRequest(syncManager, data) {
var message = SyncMessage.response.diffs;
message.content = {diffs: serializeDiff(diffs)};
socket.send(message.stringify());
syncManager.send(message.stringify());
});
}
@ -81,26 +80,25 @@ function handleResponse(syncManager, data) {
var fs = syncManager.fs;
var sync = syncManager.sync;
var session = syncManager.session;
var socket = syncManager.socket;
function resendChecksums() {
if(!session.srcList) {
// Sourcelist was somehow reset, the entire downstream sync
// needs to be restarted
session.step = steps.FAILED;
socket.send(SyncMessage.response.reset.stringify());
syncManager.send(SyncMessage.response.reset.stringify());
return onError(syncManager, new Error('Fatal Error: Could not sync filesystem from server...trying again!'));
}
rsync.checksums(fs, session.path, session.srcList, rsyncOptions, function(err, checksums) {
if(err) {
socket.send(SyncMessage.response.reset.stringify());
syncManager.send(SyncMessage.response.reset.stringify());
return onError(syncManager, err);
}
var message = SyncMessage.request.diffs;
message.content = {checksums: checksums};
socket.send(message.stringify());
syncManager.send(message.stringify());
});
}
@ -112,7 +110,7 @@ function handleResponse(syncManager, data) {
rsync.sourceList(fs, session.path, rsyncOptions, function(err, srcList) {
if(err){
socket.send(SyncMessage.request.reset.stringify());
syncManager.send(SyncMessage.request.reset.stringify());
return onError(syncManager, err);
}
@ -120,7 +118,7 @@ function handleResponse(syncManager, data) {
var message = SyncMessage.request.chksum;
message.content = {srcList: srcList};
socket.send(message.stringify());
syncManager.send(message.stringify());
});
}
@ -147,7 +145,7 @@ function handleResponse(syncManager, data) {
rsync.patch(fs, session.path, diffs, rsyncOptions, function(err, paths) {
if (err) {
var message = SyncMessage.response.reset;
socket.send(message.stringify());
syncManager.send(message.stringify());
return onError(syncManager, err);
}
@ -156,13 +154,13 @@ function handleResponse(syncManager, data) {
rsync.pathChecksums(fs, paths.synced, size, function(err, checksums) {
if(err) {
var message = SyncMessage.response.reset;
socket.send(message.stringify());
syncManager.send(message.stringify());
return onError(syncManager, err);
}
var message = SyncMessage.response.patch;
message.content = {checksums: checksums, size: size};
socket.send(message.stringify());
syncManager.send(message.stringify());
});
});
}
@ -176,7 +174,7 @@ function handleResponse(syncManager, data) {
function handleUpstreamResetResponse() {
var message = SyncMessage.request.sync;
message.content = {path: session.path};
socket.send(message.stringify());
syncManager.send(message.stringify());
}
if(data.is.sync) {
@ -201,7 +199,6 @@ function handleResponse(syncManager, data) {
function handleError(syncManager, data) {
var sync = syncManager.sync;
var session = syncManager.session;
var socket = syncManager.socket;
var message = SyncMessage.response.reset;
// DOWNSTREAM - ERROR
@ -210,10 +207,10 @@ function handleError(syncManager, data) {
session.state = states.READY;
session.step = steps.SYNCED;
socket.send(message.stringify());
syncManager.send(message.stringify());
onError(syncManager, new Error('Could not sync filesystem from server... trying again'));
} else if(data.is.verification && session.is.patch && session.is.ready) {
socket.send(message.stringify());
syncManager.send(message.stringify());
onError(syncManager, new Error('Could not sync filesystem from server... trying again'));
} else if(data.is.locked && session.is.ready && session.is.synced) {
// UPSTREAM - LOCK
@ -223,7 +220,7 @@ function handleError(syncManager, data) {
session.is.syncing) {
// UPSTREAM - ERROR
var message = SyncMessage.request.reset;
socket.send(message.stringify());
syncManager.send(message.stringify());
onError(syncManager, new Error('Could not sync filesystem from server... trying again'));
} else {
onError(syncManager, new Error('Failed to sync with the server. Current step is: ' +

Просмотреть файл

@ -5,6 +5,7 @@ var SyncMessage = require( '../../lib/syncmessage' ),
WebSocket = require('ws'),
fsUtils = require('../../lib/fs-utils'),
async = require('../../lib/async-lite.js');
var request = require('request');
function SyncManager(sync, fs) {
var manager = this;
@ -54,14 +55,16 @@ function SyncManager(sync, fs) {
};
}
SyncManager.prototype.init = function(url, token, callback) {
SyncManager.prototype.init = function(url, token, options, callback) {
var manager = this;
var session = manager.session;
var sync = manager.sync;
var reconnectCounter = 0;
var socket;
var timeout;
function handleAuth(event) {
var data = event.data || event;
try {
data = JSON.parse(data);
data = SyncMessage.parse(data);
@ -77,7 +80,7 @@ SyncManager.prototype.init = function(url, token, callback) {
var data = event.data || event;
messageHandler(manager, data);
};
socket.send(SyncMessage.response.authz.stringify());
manager.send(SyncMessage.response.authz.stringify());
callback();
} else {
@ -96,12 +99,97 @@ SyncManager.prototype.init = function(url, token, callback) {
sync.onDisconnected();
}
var socket = manager.socket = new WebSocket(url);
socket.onmessage = handleAuth;
socket.onclose = handleClose;
socket.onopen = function() {
socket.send(JSON.stringify({token: token}));
};
// Reconnecting WebSocket options
var reconnectAttempts;
var reconnectionDelay;
var reconnectionDelayMax;
if(options.autoReconnect) {
reconnectAttempts = options.reconnectAttempts ? options.reconnectAttempts : Math.Infinity;
reconnectionDelay = options.reconnectionDelay ? options.reconnectionDelay : 1000;
reconnectionDelayMax = options.reconnectionDelayMax ? options.reconnectionDelayMax : 5000;
}
function getToken(callback) {
// Remove WebSocket protocol from URL, and swap for http:// or https://
// ws://drive.webmaker.org/ -> http://drive.webmaker.org/api/sync
var apiSync = url.replace(/^([^\/]*\/\/)?/, function(match, p1) {
return p1 === 'wss://' ? 'https://' : 'http://';
});
// Also add /api/sync to the end:
apiSync = apiSync.replace(/\/?$/, '/api/sync');
request({
url: apiSync,
method: 'GET',
json: true,
withCredentials: true
}, function(err, msg, body) {
var statusCode;
var error;
statusCode = msg && msg.statusCode;
error = statusCode !== 200 ?
{ message: err || 'Unable to get token', code: statusCode } : null;
if(error) {
sync.onError(error);
} else{
callback(body);
}
});
}
function connect(reconnecting) {
clearTimeout(timeout);
socket = new WebSocket(url);
socket.onmessage = handleAuth;
socket.onopen = function() {
manager.socket = socket;
reconnectCounter = 0;
// We checking for `reconnecting` to see if this is their first time connecting to
// WebSocket and have provided us with a valid token. Otherwise this is a reconnecting
// to WebSocket and we will retrieve a new valid token.
if(!reconnecting && token) {
manager.send(JSON.stringify({token: token}));
} else {
getToken(function(token) {
manager.send(JSON.stringify({token: token}));
});
}
};
if(options.autoReconnect) {
socket.onclose = function() {
// Clean up after WebSocket closed.
socket.onclose = function(){};
socket.close();
socket = null;
manager.socket = null;
// We only want to emit an error once.
if(reconnectCounter === 0) {
var error = new Error('WebSocket closed unexpectedly');
sync.onError(error);
sync.onDisconnected();
}
if(reconnectAttempts < reconnectCounter) {
sync.emit('reconnect_failed');
} else {
var delay = reconnectCounter * reconnectionDelay;
delay = Math.min(delay, reconnectionDelayMax);
timeout = setTimeout(function () {
reconnectCounter++;
sync.emit('reconnecting');
connect(true);
}, delay);
}
};
} else {
socket.onclose = handleClose;
}
}
connect();
};
SyncManager.prototype.syncPath = function(path) {
@ -114,7 +202,7 @@ SyncManager.prototype.syncPath = function(path) {
syncRequest = SyncMessage.request.sync;
syncRequest.content = {path: path};
manager.socket.send(syncRequest.stringify());
manager.send(syncRequest.stringify());
};
// Remove the unsynced attribute for a list of paths
@ -159,4 +247,21 @@ SyncManager.prototype.close = function() {
}
};
SyncManager.prototype.send = function(syncMessage) {
var manager = this;
var ws = manager.socket;
if(!ws || ws.readyState !== ws.OPEN) {
sync.onError(new Error('Socket state invalid for sending'));
}
try {
ws.send(syncMessage);
} catch(err) {
// This will also emit an error.
sync.onError(err);
ws.close();
}
};
module.exports = SyncManager;

6
demo/js/angular/controllers.js поставляемый
Просмотреть файл

@ -103,6 +103,12 @@ angular.module('makedriveApp')
sync.on('syncing', function(){
console.log('syncing in progress');
});
sync.on('reconnecting', function(){
console.log('reconnecting');
});
sync.on('reconnect_failed', function() {
console.log('reconnect_failed');
});
sync.connect(param('makedrive'));
$rootScope.$on("mkfile", function(e, path) {
// Need to wait till the tree refresh then we select the node again

6
demo/js/angular/services.js поставляемый
Просмотреть файл

@ -8,7 +8,11 @@ angular
var Make = {};
$rootScope.canSave = false;
var fs = $window.MakeDrive.fs({
manual: true
manual: true,
autoReconnect: true,
reconnectionDelay: 500,
reconnectionDelayMax: 1500,
reconnectAttempts: 20
});
var sh = $window.MakeDrive.fs().Shell();
var sync = fs.sync;

Просмотреть файл

@ -62,7 +62,7 @@ describe('MakeDrive Client API', function(){
var layout = {'/file': 'data'};
var fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
var fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
var sync = fs.sync;
var everSeenSyncing = false;
@ -168,7 +168,7 @@ describe('MakeDrive Client API', function(){
it('should restart a downstream sync on receiving a CHKSUM ERROR SyncMessage instead of a sourceList.', function(done){
function clientLogic() {
var fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
var fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
var sync = fs.sync;
sync.on('error', function(err) {
// Confirm our client-side error is emitted as expected
@ -220,7 +220,7 @@ describe('MakeDrive Client API', function(){
var sync;
function clientLogic() {
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
sync = fs.sync;
sync.on('error', function(err) {
// Confirm our client-side error is emitted as expected
@ -285,7 +285,7 @@ describe('MakeDrive Client API', function(){
var sync;
function clientLogic() {
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
sync = fs.sync;
sync.on('error', function(err) {
// Confirm our client-side error is emitted as expected
@ -354,7 +354,7 @@ describe('MakeDrive Client API', function(){
var sync;
function clientLogic() {
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
sync = fs.sync;
sync.on('error', function(err) {
// Confirm our client-side error is emitted as expected
@ -432,7 +432,7 @@ describe('MakeDrive Client API', function(){
var fs;
var sync;
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
sync = fs.sync;
sync.once('connected', function onConnected() {
@ -468,7 +468,7 @@ describe('MakeDrive Client API', function(){
var fs;
var sync;
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
sync = fs.sync;
sync.once('connected', function onConnected() {
@ -504,7 +504,7 @@ describe('MakeDrive Client API', function(){
var fs;
var sync;
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
sync = fs.sync;
sync.once('connected', function onConnected() {
@ -531,7 +531,7 @@ describe('MakeDrive Client API', function(){
var fs;
var sync;
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true});
fs = MakeDrive.fs({provider: provider, manual: true, forceCreate: true, autoReconnect: false});
sync = fs.sync;
sync.once('connected', function onConnected() {