makedrive-client
Version:
Webmaker cloud-syncing filesystem client-side API
1,617 lines (1,431 loc) • 1.06 MB
JavaScript
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.MakeDrive=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
(function (global){
/**
* In node.js we want to use the ws module for WebSocket. In the
* browser we can just use the native WebSocket. Here we adapt
* the browser's WebSocket interface to more closely match ws
* so that we can use either.
*
* This module gets used by browserify, see package.json
*/
global.WebSocket.prototype.on = global.WebSocket.prototype.on || function(event, listener) {
this.addEventListener(event, listener);
};
global.WebSocket.prototype.removeListener = global.WebSocket.prototype.removeListener || function(event, listener) {
this.removeEventListener(event, listener);
};
global.WebSocket.prototype.once = global.WebSocket.prototype.once || function(event, listener) {
var ws = this;
this.addEventListener(event, function onEvent() {
ws.removeEventListener(event, onEvent);
listener.apply(null, arguments);
});
};
module.exports = global.WebSocket;
}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],2:[function(_dereq_,module,exports){
(function (global){
/**
* MakeDrive is a single/shared Filer filesystem instance with
* manual- and auto-sync'ing features. A client first gets the
* filesystem instance like so:
*
* var fs = MakeDrive.fs();
*
* Multiple calls to MakeDrive.fs() will return the same instance.
*
* A number of configuration options can be passed to the fs() function.
* These include:
*
* - 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.
*
* - 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.
*
* Various bits of Filer are available on MakeDrive, including:
*
* - MakeDrive.Buffer
* - MakeDrive.Path
* - MakeDrive.Errors
*
* The filesystem instance returned by MakeDrive.fs() also includes
* a new property `sync`. The fs.sync property is an EventEmitter
* which emits the following events:
*
* - 'error': an error occured while connecting/syncing. The error
* object is passed as the first arg to the event.
*
* - 'connected': a connection was established with the sync server
*
* - '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.
*
*
* The `sync` property also exposes a number of methods, including:
*
* - connect(url, [token]): try to connect to the specified sync server URL.
* An 'error' or 'connected' event will follow, depending on success. If the
* token parameter is provided, that authentication token will be used. Otherwise
* the client will try to obtain one from the server's /api/sync route. This
* requires the user to be authenticated previously with Webmaker.
*
* - disconnect(): disconnect from the sync server.
*
* - request(path): request a sync with the server for the specified
* path. Such requests may or may not be processed right away.
*
*
* Finally, the `sync` propery also exposes a `state`, which is the
* current sync state and can be one of:
*
* sync.SYNC_DISCONNECTED = 0 (also the initial state)
* sync.SYNC_CONNECTING = 1
* sync.SYNC_CONNECTED = 2
* sync.SYNC_SYNCING = 3
* sync.SYNC_ERROR = 4
*/
var SyncManager = _dereq_('./sync-manager.js');
var SyncFileSystem = _dereq_('./sync-filesystem.js');
var Filer = _dereq_('../../lib/filer.js');
var resolvePath = _dereq_('../../lib/sync-path-resolver').resolve;
var EventEmitter = _dereq_('events').EventEmitter;
var request = _dereq_('request');
var MakeDrive = {};
module.exports = MakeDrive;
function createFS(options) {
options.manual = options.manual === true;
options.memory = options.memory === true;
// Use a supplied provider, in memory RAM disk, or Fallback provider (default).
var provider;
if(options.provider) {
provider = options.provider;
} else if(options.memory) {
provider = new Filer.FileSystem.providers.Memory('makedrive');
} else {
provider = new Filer.FileSystem.providers.Fallback('makedrive');
}
// Our fs instance is a modified Filer fs, with extra sync awareness
// for conflict mediation, etc. We keep an internal reference to the
// raw Filer fs, and use the SyncFileSystem instance externally.
var _fs = new Filer.FileSystem({provider: provider});
var fs = new SyncFileSystem(_fs);
var sync = fs.sync = new EventEmitter();
var manager;
// Auto-sync handles
var autoSync;
var pathCache;
// State of the sync connection
sync.SYNC_DISCONNECTED = 0;
sync.SYNC_CONNECTING = 1;
sync.SYNC_CONNECTED = 2;
sync.SYNC_SYNCING = 3;
sync.SYNC_ERROR = 4;
// Intitially we are not connected
sync.state = sync.SYNC_DISCONNECTED;
// Turn on auto-syncing if its not already on
sync.auto = function(interval) {
var syncInterval = interval|0 > 0 ? interval|0 : 60 * 1000;
if(autoSync) {
clearInterval(autoSync);
}
autoSync = setInterval(sync.request, syncInterval);
};
// Turn off auto-syncing and turn on manual syncing
sync.manual = function() {
if(autoSync) {
clearInterval(autoSync);
autoSync = null;
}
};
sync.onError = function(err) {
// Regress to the path that needed to be synced but failed
// (likely because of a sync LOCK)
fs.pathToSync = pathCache;
sync.state = sync.SYNC_ERROR;
sync.emit('error', err);
};
sync.onDisconnected = function() {
sync.state = sync.SYNC_DISCONNECTED;
sync.emit('disconnected');
};
// Request that a sync begin.
sync.request = function() {
// If we're not connected (or are already syncing), ignore this request
if(sync.state === sync.SYNC_DISCONNECTED || sync.state === sync.SYNC_ERROR) {
sync.emit('error', new Error('Invalid state. Expected ' + sync.SYNC_CONNECTED + ', got ' + sync.state));
return;
}
// If there were no changes to the filesystem, ignore this request
if(!fs.pathToSync) {
return;
}
// Cache the path that needs to be synced for error recovery
pathCache = fs.pathToSync;
fs.pathToSync = null;
manager.syncPath(pathCache);
};
// Try to connect to the server.
sync.connect = function(url, token) {
// Bail if we're already connected
if(sync.state !== sync.SYNC_DISCONNECTED &&
sync.state !== sync.ERROR) {
sync.emit('error', new Error("MakeDrive: Attempted to connect to \"" + url + "\", but a connection already exists!"));
return;
}
// Also bail if we already have a SyncManager
if(manager) {
return;
}
// Upgrade connection state to `connecting`
sync.state = sync.SYNC_CONNECTING;
function downstreamSyncCompleted() {
// Re-wire message handler functions for regular syncing
// now that initial downstream sync is completed.
sync.onSyncing = function() {
sync.state = sync.SYNC_SYNCING;
sync.emit('syncing');
};
sync.onCompleted = function(paths) {
// If changes happened to the files that needed to be synced
// during the sync itself, they will be overwritten
// https://github.com/mozilla/makedrive/issues/129 and
// https://github.com/mozilla/makedrive/issues/3
function complete() {
sync.state = sync.SYNC_CONNECTED;
sync.emit('completed');
}
if(!paths) {
return complete();
}
manager.resetUnsynced(paths, function(err) {
if(err) {
return sync.onError(err);
}
complete();
});
};
// Upgrade connection state to 'connected'
sync.state = sync.SYNC_CONNECTED;
// If we're in manual mode, bail before starting auto-sync
if(options.manual) {
sync.manual();
} else {
sync.auto(options.interval);
}
sync.emit('connected');
}
function connect(token) {
// Try to connect to provided server URL. Use the raw Filer fs
// 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) {
if(err) {
sync.onError(err);
return;
}
// In a browser, try to clean-up after ourselves when window goes away
if("onbeforeunload" in global) {
sync.cleanupFn = function() {
if(manager) {
manager.close();
manager = null;
}
};
global.addEventListener('beforeunload', sync.cleanupFn);
}
// Wait on initial downstream sync events to complete
sync.onSyncing = function() {
// do nothing, wait for onCompleted()
};
sync.onCompleted = function() {
// Downstream sync is done, finish connect() setup
downstreamSyncCompleted();
};
});
}
// 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);
}
});
}
};
// Disconnect from the server
sync.disconnect = function() {
// Remove our browser cleanup
if("onbeforeunload" in global && sync.cleanupFn) {
global.removeEventListener('beforeunload', sync.cleanupFn);
sync.cleanupFn = null;
}
// Bail if we're not already connected
if(sync.state === sync.SYNC_DISCONNECTED ||
sync.state === sync.ERROR) {
sync.emit('error', new Error("MakeDrive: Attempted to disconnect, but no server connection exists!"));
return;
}
// Do a proper shutdown
if(manager) {
manager.close();
manager = null;
}
// Stop auto-syncing
if(autoSync) {
clearInterval(autoSync);
autoSync = null;
fs.pathToSync = null;
}
sync.onDisconnected();
};
return fs;
}
// Manage single instance of a Filer filesystem with auto-sync'ing
var sharedFS;
MakeDrive.fs = function(options) {
options = options || {};
// We usually only want to hand out a single, shared instance
// for every call, but sometimes you need multiple (e.g., tests)
if(options.forceCreate) {
return createFS(options);
}
if(!sharedFS) {
sharedFS = createFS(options);
}
return sharedFS;
};
// Expose bits of Filer that clients will need on MakeDrive
MakeDrive.Buffer = Filer.Buffer;
MakeDrive.Path = Filer.Path;
MakeDrive.Errors = Filer.Errors;
}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"../../lib/filer.js":72,"../../lib/sync-path-resolver":75,"./sync-filesystem.js":4,"./sync-manager.js":5,"events":120,"request":81}],3:[function(_dereq_,module,exports){
var SyncMessage = _dereq_('../../lib/syncmessage');
var rsync = _dereq_('../../lib/rsync');
var rsyncOptions = _dereq_('../../lib/constants').rsyncDefaults;
var serializeDiff = _dereq_('../../lib/diff').serialize;
var deserializeDiff = _dereq_('../../lib/diff').deserialize;
var states = _dereq_('./sync-states');
var steps = _dereq_('./sync-steps');
function onError(syncManager, err) {
syncManager.session.step = steps.FAILED;
syncManager.sync.onError(err);
}
function handleRequest(syncManager, data) {
var fs = syncManager.fs;
var sync = syncManager.sync;
var session = syncManager.session;
var socket = syncManager.socket;
function handleChecksumRequest() {
var srcList = data.content.srcList;
session.path = data.content.path;
sync.onSyncing();
rsync.checksums(fs, session.path, srcList, rsyncOptions, function(err, checksums) {
if (err) {
return onError(syncManager, err);
}
session.step = steps.PATCH;
var message = SyncMessage.request.diffs;
message.content = {checksums: checksums};
socket.send(message.stringify());
});
}
function handleDiffRequest() {
rsync.diff(fs, session.path, data.content.checksums, rsyncOptions, function(err, diffs) {
if(err){
return onError(syncManager, err);
}
session.step = steps.PATCH;
var message = SyncMessage.response.diffs;
message.content = {diffs: serializeDiff(diffs)};
socket.send(message.stringify());
});
}
if(data.is.chksum && session.is.ready &&
(session.is.synced || session.is.failed)) {
// DOWNSTREAM - CHKSUM
handleChecksumRequest();
} else if(data.is.diffs && session.is.syncing && session.is.diffs) {
// UPSTREAM - DIFFS
handleDiffRequest();
} else {
onError(syncManager, new Error(data.content));
}
}
function handleResponse(syncManager, data) {
var fs = syncManager.fs;
var sync = syncManager.sync;
var session = syncManager.session;
var socket = syncManager.socket;
function handleSrcListResponse() {
session.state = states.SYNCING;
session.step = steps.INIT;
session.path = data.content.path;
sync.onSyncing();
rsync.sourceList(fs, session.path, rsyncOptions, function(err, srcList) {
if(err){
return onError(syncManager, err);
}
session.step = steps.DIFFS;
var message = SyncMessage.request.chksum;
message.content = {srcList: srcList};
socket.send(message.stringify());
});
}
function handlePatchAckResponse() {
session.state = states.READY;
session.step = steps.SYNCED;
sync.onCompleted(data.content.syncedPaths);
}
function handlePatchResponse() {
var diffs = data.content.diffs;
diffs = deserializeDiff(diffs);
rsync.patch(fs, session.path, diffs, rsyncOptions, function(err, paths) {
if (err) {
return onError(syncManager, err);
}
var size = rsyncOptions.size || 5;
rsync.pathChecksums(fs, paths.synced, size, function(err, checksums) {
if(err) {
return onError(syncManager, err);
}
var message = SyncMessage.response.patch;
message.content = {checksums: checksums, size: size};
socket.send(message.stringify());
});
});
}
function handleVerificationResponse() {
session.step = steps.SYNCED;
sync.onCompleted();
}
if(data.is.sync) {
// UPSTREAM - INIT
handleSrcListResponse();
} else if(data.is.patch && session.is.syncing && session.is.patch) {
// UPSTREAM - PATCH
handlePatchAckResponse();
} else if(data.is.diffs && session.is.ready && session.is.patch) {
// DOWNSTREAM - PATCH
handlePatchResponse();
} else if(data.is.verification && session.is.ready && session.is.patch) {
// DOWNSTREAM - PATCH VERIFICATION
handleVerificationResponse();
} else {
onError(syncManager, new Error(data.content));
}
}
function handleError(syncManager, data) {
var sync = syncManager.sync;
var session = syncManager.session;
var socket = syncManager.socket;
// DOWNSTREAM - ERROR
if((((data.is.srclist && session.is.synced) ||
(data.is.verification && session.is.synced)) &&
session.is.ready) ||
(data.is.diffs && session.is.patch && (session.is.ready || session.is.syncing))) {
session.state = states.READY;
session.step = steps.SYNCED;
var message = SyncMessage.request.reset;
socket.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
onError(syncManager, new Error('Current sync in progress! Try again later!'));
} else if(((data.is.chksum && session.is.diffs) ||
(data.is.patch && session.is.patch)) &&
session.is.syncing) {
// UPSTREAM - ERROR
onError(syncManager, new Error('Fatal error: Failed to sync to server'));
} else {
onError(syncManager, new Error(data.content));
}
}
function handleMessage(syncManager, data) {
try {
data = JSON.parse(data);
data = SyncMessage.parse(data);
} catch(e) {
return onError(syncManager, e);
}
if (data.is.request) {
handleRequest(syncManager, data);
} else if(data.is.response){
handleResponse(syncManager, data);
} else if(data.is.error){
handleError(syncManager, data);
} else {
onError(syncManager, new Error('Cannot handle message'));
}
}
module.exports = handleMessage;
},{"../../lib/constants":69,"../../lib/diff":70,"../../lib/rsync":74,"../../lib/syncmessage":76,"./sync-states":6,"./sync-steps":7}],4:[function(_dereq_,module,exports){
/**
* An extended Filer FileSystem with wrapped methods
* for writing that manage file metadata (xattribs)
* reflecting sync state.
*/
var Filer = _dereq_('../../lib/filer.js');
var Shell = _dereq_('../../lib/filer-shell.js');
var Path = Filer.Path;
var fsUtils = _dereq_('../../lib/fs-utils.js');
var conflict = _dereq_('../../lib/conflict.js');
var constants = _dereq_('../../lib/constants.js');
var resolvePath = _dereq_('../../lib/sync-path-resolver.js').resolve;
function SyncFileSystem(fs) {
var self = this;
var pathToSync;
// Manage path resolution for sync path
Object.defineProperty(self, 'pathToSync', {
get: function() { return pathToSync; },
set: function(path) {
if(path) {
pathToSync = resolvePath(pathToSync, path);
} else {
pathToSync = null;
}
}
});
// The following non-modifying fs operations can be run as normal,
// and are simply forwarded to the fs instance. NOTE: we have
// included setting xattributes since we don't sync these to the server (yet).
['stat', 'fstat', 'lstat', 'exists', 'readlink', 'realpath',
'readdir', 'open', 'close', 'fsync', 'read', 'readFile',
'setxattr', 'fsetxattr', 'getxattr', 'fgetxattr', 'removexattr',
'fremovexattr', 'watch'].forEach(function(method) {
self[method] = function() {
fs[method].apply(fs, arguments);
};
});
function fsetUnsynced(fd, callback) {
fsUtils.fsetUnsynced(fs, fd, callback);
}
function setUnsynced(path, callback) {
fsUtils.setUnsynced(fs, path, callback);
}
// We wrap all fs methods that modify the filesystem in some way that matters
// for syncing (i.e., changes we need to sync back to the server), such that we
// can track things. Different fs methods need to do this in slighly different ways,
// but the overall logic is the same. The wrapMethod() fn defines this logic.
function wrapMethod(method, pathArgPos, setUnsyncedFn, useParentPath) {
return function() {
var args = Array.prototype.slice.call(arguments, 0);
var lastIdx = args.length - 1;
var callback = args[lastIdx];
// Grab the path or fd so we can use it to set the xattribute.
// Most methods take `path` or `fd` as the first arg, but it's
// second for some.
var pathOrFD = args[pathArgPos];
// In most cases we want to use the path itself, but in the case
// that a node is being removed, we want the parent dir.
pathOrFD = useParentPath ? Path.dirname(pathOrFD) : pathOrFD;
// Check to see if it is a path or an open file descriptor
// TODO: Deal with a case of fs.open for a path with a write flag
// https://github.com/mozilla/makedrive/issues/210.
if(!fs.openFiles[pathOrFD]) {
self.pathToSync = pathOrFD;
}
args[lastIdx] = function wrappedCallback() {
var args = Array.prototype.slice.call(arguments, 0);
if(args[0]) {
return callback(args[0]);
}
setUnsyncedFn(pathOrFD, function(err) {
if(err) {
return callback(err);
}
callback.apply(null, args);
});
};
fs[method].apply(fs, args);
};
}
// Wrapped fs methods that have path at first arg position and use paths
['truncate', 'mknod', 'mkdir', 'utimes', 'writeFile',
'appendFile'].forEach(function(method) {
self[method] = wrapMethod(method, 0, setUnsynced);
});
// Wrapped fs methods that have path at second arg position
['rename', 'link', 'symlink'].forEach(function(method) {
self[method] = wrapMethod(method, 1, setUnsynced);
});
// Wrapped fs methods that use file descriptors
['ftruncate', 'futimes', 'write'].forEach(function(method) {
self[method] = wrapMethod(method, 0, fsetUnsynced);
});
// Wrapped fs methods that have path at first arg position and use parent
// path for writing unsynced metadata (i.e., removes node)
['rmdir', 'unlink'].forEach(function(method) {
self[method] = wrapMethod(method, 0, setUnsynced, true);
});
// We also want to do extra work in the case of a rename.
// If a file is a conflicted copy, and a rename is done,
// remove the conflict.
var rename = self.rename;
self.rename = function(oldPath, newPath, callback) {
rename(oldPath, newPath, function(err) {
if(err) {
return callback(err);
}
conflict.isConflictedCopy(fs, newPath, function(err, conflicted) {
if(err) {
return callback(err);
}
if(conflicted) {
conflict.removeFileConflict(fs, newPath, callback);
} else {
callback();
}
});
});
};
// Expose fs.Shell() but use wrapped sync filesystem instance vs fs.
// This is a bit brittle, but since Filer doesn't expose the Shell()
// directly, we deal with it by doing a deep require into Filer's code
// ourselves. The other down side of this is that we're now including
// the Shell code twice (once in filer.js, once here). We need to
// optimize this when we look at making MakeDrive smaller.
self.Shell = function(options) {
return new Shell(self, options);
};
// Expose extra operations for checking whether path/fd is unsynced
self.getUnsynced = function(path, callback) {
fsUtils.getUnsynced(fs, path, callback);
};
self.fgetUnsynced = function(fd, callback) {
fsUtils.fgetUnsynced(fs, fd, callback);
};
}
module.exports = SyncFileSystem;
},{"../../lib/conflict.js":68,"../../lib/constants.js":69,"../../lib/filer-shell.js":71,"../../lib/filer.js":72,"../../lib/fs-utils.js":73,"../../lib/sync-path-resolver.js":75}],5:[function(_dereq_,module,exports){
var SyncMessage = _dereq_( '../../lib/syncmessage' ),
messageHandler = _dereq_('./message-handler'),
states = _dereq_('./sync-states'),
steps = _dereq_('./sync-steps'),
WebSocket = _dereq_('ws'),
fsUtils = _dereq_('../../lib/fs-utils'),
async = _dereq_('async');
function SyncManager(sync, fs) {
var manager = this;
manager.sync = sync;
manager.fs = fs;
manager.session = {
state: states.CLOSED,
step: steps.SYNCED,
path: '/',
is: Object.create(Object.prototype, {
// States
syncing: {
get: function() { return manager.session.state === states.SYNCING; }
},
ready: {
get: function() { return manager.session.state === states.READY; }
},
error: {
get: function() { return manager.session.state === states.ERROR; }
},
closed: {
get: function() { return manager.session.state === states.CLOSED; }
},
// Steps
init: {
get: function() { return manager.session.step === steps.INIT; }
},
chksum: {
get: function() { return manager.session.step === steps.CHKSUM; }
},
diffs: {
get: function() { return manager.session.step === steps.DIFFS; }
},
patch: {
get: function() { return manager.session.step === steps.PATCH; }
},
synced: {
get: function() { return manager.session.step === steps.SYNCED; }
},
failed: {
get: function() { return manager.session.step === steps.FAILED; }
}
})
};
}
SyncManager.prototype.init = function(url, token, callback) {
var manager = this;
var session = manager.session;
var sync = manager.sync;
function handleAuth(event) {
var data = event.data || event;
try {
data = JSON.parse(data);
data = SyncMessage.parse(data);
} catch(e) {
return callback(e);
}
if(data.is.response && data.is.authz) {
session.state = states.READY;
session.step = steps.SYNCED;
socket.onmessage = function(event) {
var data = event.data || event;
messageHandler(manager, data);
};
socket.send(SyncMessage.response.authz.stringify());
callback();
} else {
callback(new Error('Cannot handle message'));
}
}
function handleClose(info) {
var reason = info.reason || 'WebSocket closed unexpectedly';
var error = new Error(info.code + ': ' + reason);
manager.close();
manager.socket = null;
sync.onError(error);
sync.onDisconnected();
}
var socket = manager.socket = new WebSocket(url);
socket.onmessage = handleAuth;
socket.onclose = handleClose;
socket.onopen = function() {
socket.send(JSON.stringify({token: token}));
};
};
SyncManager.prototype.syncPath = function(path) {
var manager = this;
var syncRequest;
if(!manager.socket) {
throw new Error('sync called before init');
}
syncRequest = SyncMessage.request.sync;
syncRequest.content = {path: path};
manager.socket.send(syncRequest.stringify());
};
// Remove the unsynced attribute for a list of paths
SyncManager.prototype.resetUnsynced = function(paths, callback) {
var fs = this.fs;
function removeUnsyncedAttr(path, callback) {
fsUtils.removeUnsynced(fs, path, function(err) {
if(err && err.code !== 'ENOENT') {
return callback(err);
}
callback();
});
}
async.eachSeries(paths, removeUnsyncedAttr, function(err) {
if(err) {
return callback(err);
}
callback();
});
};
SyncManager.prototype.close = function() {
var manager = this;
var socket = manager.socket;
if(socket) {
socket.onmessage = function(){};
socket.onopen = function(){};
if(socket.readyState === 1) {
socket.onclose = function(){
manager.socket = null;
};
socket.close();
} else {
manager.socket = null;
}
}
};
module.exports = SyncManager;
},{"../../lib/fs-utils":73,"../../lib/syncmessage":76,"./message-handler":3,"./sync-states":6,"./sync-steps":7,"async":80,"ws":1}],6:[function(_dereq_,module,exports){
module.exports = {
SYNCING: "SYNC IN PROGRESS",
READY: "READY",
ERROR: "ERROR",
CLOSED: "CLOSED"
};
},{}],7:[function(_dereq_,module,exports){
module.exports = {
INIT: "SYNC INITIALIZED",
CHKSUM: "CHECKSUM",
DIFFS: "DIFFS",
PATCH: "PATCH",
SYNCED: "SYNCED",
FAILED: "FAILED"
};
},{}],8:[function(_dereq_,module,exports){
(function (process){
/*global setImmediate: false, setTimeout: false, console: false */
/**
* https://raw.github.com/caolan/async/master/lib/async.js Feb 18, 2014
* Used under MIT - https://github.com/caolan/async/blob/master/LICENSE
*/
(function () {
var async = {};
// global on the server, window in the browser
var root, previous_async;
root = this;
if (root != null) {
previous_async = root.async;
}
async.noConflict = function () {
root.async = previous_async;
return async;
};
function only_once(fn) {
var called = false;
return function() {
if (called) throw new Error("Callback was already called.");
called = true;
fn.apply(root, arguments);
}
}
//// cross-browser compatiblity functions ////
var _each = function (arr, iterator) {
if (arr.forEach) {
return arr.forEach(iterator);
}
for (var i = 0; i < arr.length; i += 1) {
iterator(arr[i], i, arr);
}
};
var _map = function (arr, iterator) {
if (arr.map) {
return arr.map(iterator);
}
var results = [];
_each(arr, function (x, i, a) {
results.push(iterator(x, i, a));
});
return results;
};
var _reduce = function (arr, iterator, memo) {
if (arr.reduce) {
return arr.reduce(iterator, memo);
}
_each(arr, function (x, i, a) {
memo = iterator(memo, x, i, a);
});
return memo;
};
var _keys = function (obj) {
if (Object.keys) {
return Object.keys(obj);
}
var keys = [];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
keys.push(k);
}
}
return keys;
};
//// exported async module functions ////
//// nextTick implementation with browser-compatible fallback ////
if (typeof process === 'undefined' || !(process.nextTick)) {
if (typeof setImmediate === 'function') {
async.nextTick = function (fn) {
// not a direct alias for IE10 compatibility
setImmediate(fn);
};
async.setImmediate = async.nextTick;
}
else {
async.nextTick = function (fn) {
setTimeout(fn, 0);
};
async.setImmediate = async.nextTick;
}
}
else {
async.nextTick = process.nextTick;
if (typeof setImmediate !== 'undefined') {
async.setImmediate = function (fn) {
// not a direct alias for IE10 compatibility
setImmediate(fn);
};
}
else {
async.setImmediate = async.nextTick;
}
}
async.each = function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
_each(arr, function (x) {
iterator(x, only_once(function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed >= arr.length) {
callback(null);
}
}
}));
});
};
async.forEach = async.each;
async.eachSeries = function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
var iterate = function () {
iterator(arr[completed], function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed >= arr.length) {
callback(null);
}
else {
iterate();
}
}
});
};
iterate();
};
async.forEachSeries = async.eachSeries;
async.eachLimit = function (arr, limit, iterator, callback) {
var fn = _eachLimit(limit);
fn.apply(null, [arr, iterator, callback]);
};
async.forEachLimit = async.eachLimit;
var _eachLimit = function (limit) {
return function (arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length || limit <= 0) {
return callback();
}
var completed = 0;
var started = 0;
var running = 0;
(function replenish () {
if (completed >= arr.length) {
return callback();
}
while (running < limit && started < arr.length) {
started += 1;
running += 1;
iterator(arr[started - 1], function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
running -= 1;
if (completed >= arr.length) {
callback();
}
else {
replenish();
}
}
});
}
})();
};
};
var doParallel = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [async.each].concat(args));
};
};
var doParallelLimit = function(limit, fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [_eachLimit(limit)].concat(args));
};
};
var doSeries = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return fn.apply(null, [async.eachSeries].concat(args));
};
};
var _asyncMap = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (err, v) {
results[x.index] = v;
callback(err);
});
}, function (err) {
callback(err, results);
});
};
async.map = doParallel(_asyncMap);
async.mapSeries = doSeries(_asyncMap);
async.mapLimit = function (arr, limit, iterator, callback) {
return _mapLimit(limit)(arr, iterator, callback);
};
var _mapLimit = function(limit) {
return doParallelLimit(limit, _asyncMap);
};
// reduce only has a series version, as doing reduce in parallel won't
// work in many situations.
async.reduce = function (arr, memo, iterator, callback) {
async.eachSeries(arr, function (x, callback) {
iterator(memo, x, function (err, v) {
memo = v;
callback(err);
});
}, function (err) {
callback(err, memo);
});
};
// inject alias
async.inject = async.reduce;
// foldl alias
async.foldl = async.reduce;
async.reduceRight = function (arr, memo, iterator, callback) {
var reversed = _map(arr, function (x) {
return x;
}).reverse();
async.reduce(reversed, memo, iterator, callback);
};
// foldr alias
async.foldr = async.reduceRight;
var _filter = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (v) {
if (v) {
results.push(x);
}
callback();
});
}, function (err) {
callback(_map(results.sort(function (a, b) {
return a.index - b.index;
}), function (x) {
return x.value;
}));
});
};
async.filter = doParallel(_filter);
async.filterSeries = doSeries(_filter);
// select alias
async.select = async.filter;
async.selectSeries = async.filterSeries;
var _reject = function (eachfn, arr, iterator, callback) {
var results = [];
arr = _map(arr, function (x, i) {
return {index: i, value: x};
});
eachfn(arr, function (x, callback) {
iterator(x.value, function (v) {
if (!v) {
results.push(x);
}
callback();
});
}, function (err) {
callback(_map(results.sort(function (a, b) {
return a.index - b.index;
}), function (x) {
return x.value;
}));
});
};
async.reject = doParallel(_reject);
async.rejectSeries = doSeries(_reject);
var _detect = function (eachfn, arr, iterator, main_callback) {
eachfn(arr, function (x, callback) {
iterator(x, function (result) {
if (result) {
main_callback(x);
main_callback = function () {};
}
else {
callback();
}
});
}, function (err) {
main_callback();
});
};
async.detect = doParallel(_detect);
async.detectSeries = doSeries(_detect);
async.some = function (arr, iterator, main_callback) {
async.each(arr, function (x, callback) {
iterator(x, function (v) {
if (v) {
main_callback(true);
main_callback = function () {};
}
callback();
});
}, function (err) {
main_callback(false);
});
};
// any alias
async.any = async.some;
async.every = function (arr, iterator, main_callback) {
async.each(arr, function (x, callback) {
iterator(x, function (v) {
if (!v) {
main_callback(false);
main_callback = function () {};
}
callback();
});
}, function (err) {
main_callback(true);
});
};
// all alias
async.all = async.every;
async.sortBy = function (arr, iterator, callback) {
async.map(arr, function (x, callback) {
iterator(x, function (err, criteria) {
if (err) {
callback(err);
}
else {
callback(null, {value: x, criteria: criteria});
}
});
}, function (err, results) {
if (err) {
return callback(err);
}
else {
var fn = function (left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
};
callback(null, _map(results.sort(fn), function (x) {
return x.value;
}));
}
});
};
async.auto = function (tasks, callback) {
callback = callback || function () {};
var keys = _keys(tasks);
if (!keys.length) {
return callback(null);
}
var results = {};
var listeners = [];
var addListener = function (fn) {
listeners.unshift(fn);
};
var removeListener = function (fn) {
for (var i = 0; i < listeners.length; i += 1) {
if (listeners[i] === fn) {
listeners.splice(i, 1);
return;
}
}
};
var taskComplete = function () {
_each(listeners.slice(0), function (fn) {
fn();
});
};
addListener(function () {
if (_keys(results).length === keys.length) {
callback(null, results);
callback = function () {};
}
});
_each(keys, function (k) {
var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k];
var taskCallback = function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
if (err) {
var safeResults = {};
_each(_keys(results), function(rkey) {
safeResults[rkey] = results[rkey];
});
safeResults[k] = args;
callback(err, safeResults);
// stop subsequent errors hitting callback multiple times
callback = function () {};
}
else {
results[k] = args;
async.setImmediate(taskComplete);
}
};
var requires = task.slice(0, Math.abs(task.length - 1)) || [];
var ready = function () {
return _reduce(requires, function (a, x) {
return (a && results.hasOwnProperty(x));
}, true) && !results.hasOwnProperty(k);
};
if (ready()) {
task[task.length - 1](taskCallback, results);
}
else {
var listener = function () {
if (ready()) {
removeListener(listener);
task[task.length - 1](taskCallback, results);
}
};
addListener(listener);
}
});
};
async.waterfall = function (tasks, callback) {
callback = callback || function () {};
if (tasks.constructor !== Array) {
var err = new Error('First argument to waterfall must be an array of functions');
return callback(err);
}
if (!tasks.length) {
return callback();
}
var wrapIterator = function (iterator) {
return function (err) {
if (err) {
callback.apply(null, arguments);
callback = function () {};
}
else {
var args = Array.prototype.slice.call(arguments, 1);
var next = iterator.next();
if (next) {
args.push(wrapIterator(next));
}
else {
args.push(callback);
}
async.setImmediate(function () {
iterator.apply(null, args);
});
}
};
};
wrapIterator(async.iterator(tasks))();
};
var _parallel = function(eachfn, tasks, callback) {
callback = callback || function () {};
if (tasks.constructor === Array) {
eachfn.map(tasks, function (fn, callback) {
if (fn) {
fn(function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
callback.call(null, err, args);
});
}
}, callback);
}
else {
var results = {};
eachfn.each(_keys(tasks), function (k, callback) {
tasks[k](function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
results[k] = args;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
};
async.parallel = function (tasks, callback) {
_parallel({ map: async.map, each: async.each }, tasks, callback);
};
async.parallelLimit = function(tasks, limit, callback) {
_parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback);
};
async.series = function (tasks, callback) {
callback = callback || function () {};
if (tasks.constructor === Array) {
async.mapSeries(tasks, function (fn, callback) {
if (fn) {
fn(function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
callback.call(null, err, args);
});
}
}, callback);
}
else {
var results = {};
async.eachSeries(_keys(tasks), function (k, callback) {
tasks[k](function (err) {
var args = Array.prototype.slice.call(arguments, 1);
if (args.length <= 1) {
args = args[0];
}
results[k] = args;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
};
async.iterator = function (tasks) {
var makeCallback = function (index) {
var fn = function () {
if (tasks.length) {
tasks[index].apply(null, arguments);
}
return fn.next();
};
fn.next = function () {
return (index < tasks.length - 1) ? makeCallback(index + 1): null;
};
return fn;
};
return makeCallback(0);
};
async.apply = function (fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function () {
return fn.apply(
null, args.concat(Array.prototype.slice.call(arguments))
);
};
};
var _concat = function (eachfn, arr, fn, callback) {
var r = [];
eachfn(arr, function (x, cb) {
fn(x, function (err, y) {
r = r.concat(y || []);
cb(err);
});
}, function (err) {
callback(err, r);
});
};
async.concat = doParallel(_concat);
async.concatSeries = doSeries(_concat);
async.whilst = function (test, iterator, callback) {
if (test()) {
iterator(function (err) {
if (err) {
return callback(err);
}
async.whilst(test, iterator, callback);
});
}
else {
callback();
}
};
async.doWhilst = function (iterator, test, callback) {
iterator(function (err) {
if (err) {
return callback(err);
}
if (test()) {
async.doWhilst(iterator, test, callback);
}
else {
callback();
}
});
};
async.until = function (test, iterator, callback) {
if (!test()) {
iterator(function (err) {
if (err) {
return callback(err);
}
async.until(test, iterator, callback);
});
}
else {
callback();
}
};
async.doUntil = function (iterator, test, callback) {
iterator(function (err) {
if (err) {
return callback(err);
}
if (!test()) {
async.doUntil(iterator, test, callback);
}
else {
callback();
}
});
};
async.queue = function (worker, concurrency) {
if (concurrency === undefined) {
concurrency = 1;
}
function _insert(q, data, pos, callback) {
if(data.constructor !== Array) {
data = [data];
}
_each(data, function(tas