<commit_before>'use strict';

var Q = require('q');
var read = require('read');
var fs  = require('fs');
var p = require('path');
var ffs = require('final-fs');

var Client = function() {

    this.OVERWRITE_ALWAYS   = 0;
    this.OVERWRITE_NEVER    = 1;
    this.OVERWRITE_ASK      = 2;
    this.OVERWRITE_IF_NEWER = 3;

    var that = this;

    var deleteLocalFile = function(file){
        var deferred = Q.defer();
        fs.unlink(file, function (err) {
            if (err) {
                deferred.reject(err);
            } else {
                deferred.resolve();
            }
        });
        return deferred.promise;
    };

    var deleteRemoteFile = function(project, file) {
        return project.removeFile(file);
    };

    var writeToLocalFile = function(project, file){
        var deferred = Q.defer();
        project.getFile(file).then(function(source){
            var dir = p.dirname(file);
            ffs.mkdirRecursiveSync(dir, 511);
            fs.writeFileSync(file, source, 'UTF-8', {'flags': 'w'});
            deferred.resolve();
        })
        .catch(function(error){
            deferred.reject(error);
        });
        return deferred.promise;
    };

    var writeToRemoteFile = function(project, file){
        var source = fs.readFileSync(file, 'UTF-8');
        return project.writeFile(file, source);
    };

    var performAction = function(fromFile, toFile, overwrite){
        var result = { action: true, fromFile: fromFile, toFile: toFile };
        var deferred = Q.defer();
        if(toFile.exists === true && overwrite !== that.OVERWRITE_ALWAYS) {
            if(overwrite === that.OVERWRITE_NEVER) {
                result.action = false;
                deferred.resolve(result);
            } else if(overwrite === that.OVERWRITE_ASK) {
                read(
                    {
                        prompt: 'Overwrite : <' + toFile.origin + '>/' + toFile.path +
                            ' (' + toFile.lastModified + ') with <' + fromFile.origin + '>/' +
                            fromFile.path +
                            ' (' + fromFile.lastModified + ')? Y/[N]',
                        'default': 'N'
                    },
                    function(er, value) {
                        if(er) {
                            deferred.reject(er);
                        } else if(value !== undefined) {
                            result.action = (value.toUpperCase() === 'Y');
                            deferred.resolve(result);
                        } else {
                            deferred.reject();
                        }
                    }
                );
            } else if(overwrite === that.OVERWRITE_IF_NEWER){
                result.action = (
                    new Date(fromFile.lastModified).getTime() >
                    new Date(toFile.lastModified).getTime()
                );
                deferred.resolve(result);
            }
        } else {
            deferred.resolve(result);
        }
        return deferred.promise;
    };

    var sync = function(project, from, to, overwrite, deleteOrphaned, simulate, fromOrigin, toOrigin){
        var promises = [];

        Object.keys(from).forEach(function(file){

            var fromFile = {
                path: file,
                exists: true,
                lastModified: from[file].lastModified,
                origin: fromOrigin
            };

            var toFile  = {
                path: file,
                exists: to[file] !== undefined,
                lastModified: to[file] ? to[file].lastModified : undefined,
                origin: toOrigin
            };

            promises.push(function() {
                var deferred = Q.defer();
                performAction(fromFile, toFile, overwrite).then(function(result){
                    if(result.action) {
                        console.log((fromOrigin === 'remote' ? 'Download ' : 'Upload ') + result.fromFile.path);
                        if(!simulate) {
                            (
                                fromOrigin === 'remote' ?
                                writeToLocalFile(project, result.fromFile.path) :
                                writeToRemoteFile(project, result.fromFile.path)
                            )
                            .then(function(){
                                deferred.resolve();
                            })
                            .catch(function(error){
                                deferred.reject(error);
                            });
                        } else {
                            deferred.resolve();
                        }
                    } else {
                        deferred.resolve();
                    }
                })
                .catch(function(error){
                    console.error(error);
                });
                return deferred.promise;
            });
        });

        if(deleteOrphaned) {
            Object.keys(to).forEach(function(file){
                if(!from[file]) {
                    promises.push(function(){
                        var deferred = Q.defer();
                        console.log('Delete ' + '<' + toOrigin + '>/' + file);
                        if(!simulate) {
                            (
                                fromOrigin === 'remote' ?
                                deleteLocalFile(file) :
                                deleteRemoteFile(project, file)
                            )
                            .then(function(){
                                deferred.resolve();
                            })
                            .catch(function(error){
                                deferred.reject(error);
                            });
                        } else {
                            deferred.resolve();
                        }
                        return deferred.promise;
                    });
                }
            });
        }

        if(overwrite === that.OVERWRITE_ASK) {
          return promises.reduce(Q.when, Q());
        } else {
          var newPromises = [];
          promises.forEach(function(promise){
              newPromises.push(promise());
          });
          return Q.all(newPromises);
        }
    };

    var filterList = function(list, ignore) {
        Object.keys(list).forEach(function(path){
            //Delete public or private files that don't have a .jq or .xq suffix
            if(
                (path.indexOf('public/') === 0 || path.indexOf('private/') === 0) &&
                path.substring(path.length - 3) !== '.jq' &&
                path.substring(path.length - 3) !== '.xq'
            ) {
                delete list[path];
            } else {
                var matched = false;
                ignore.forEach(function(mask){
                    if(path.match(mask) !== null) {
                        matched = true;
                        return false;
                    }
                });
                if(matched === true) {
                    delete list[path];
                }
            }
        });
        return list;
    };

    this.upload = function(project, remote, local, overwrite, deleteOrphaned, simulate, ignore){
        remote = filterList(remote, ignore);
        local  = filterList(local, ignore);
        return sync(
            project, local, remote, overwrite,
            deleteOrphaned, simulate, 'local', 'remote'
        );
    };

    this.download = function(project, remote, local, overwrite, deleteOrphaned, simulate, ignore){
        remote = filterList(remote, ignore);
        local  = filterList(local, ignore);
        return sync(
            project, remote, local, overwrite,
            deleteOrphaned, simulate, 'remote', 'local'
        );
    };

    this.truncate = function(project, remote, simulate, ignore){
        remote = filterList(remote, ignore);
        return sync(
            project, {}, remote, this.OVERWRITE_ALWAYS,
            true, simulate, 'local', 'remote'
        );
    };
};

module.exports = new Client();
<commit_msg>Execute request asynchrnous even in the overwrite = ask scenario.<commit_after>'use strict';

var Q = require('q');
var read = require('read');
var fs  = require('fs');
var p = require('path');
var ffs = require('final-fs');

var Client = function() {

    this.OVERWRITE_ALWAYS   = 0;
    this.OVERWRITE_NEVER    = 1;
    this.OVERWRITE_ASK      = 2;
    this.OVERWRITE_IF_NEWER = 3;

    var that = this;

    var deleteLocalFile = function(file){
        var deferred = Q.defer();
        fs.unlink(file, function (err) {
            if (err) {
                deferred.reject(err);
            } else {
                deferred.resolve();
            }
        });
        return deferred.promise;
    };

    var deleteRemoteFile = function(project, file) {
        return project.removeFile(file);
    };

    var writeToLocalFile = function(project, file){
        var deferred = Q.defer();
        project.getFile(file).then(function(source){
            var dir = p.dirname(file);
            ffs.mkdirRecursiveSync(dir, 511);
            fs.writeFileSync(file, source, 'UTF-8', {'flags': 'w'});
            deferred.resolve();
        })
        .catch(function(error){
            deferred.reject(error);
        });
        return deferred.promise;
    };

    var writeToRemoteFile = function(project, file){
        var source = fs.readFileSync(file, 'UTF-8');
        return project.writeFile(file, source);
    };

    var performAction = function(fromFile, toFile, overwrite){
        var result = { action: true, fromFile: fromFile, toFile: toFile };
        var deferred = Q.defer();
        if(toFile.exists === true && overwrite !== that.OVERWRITE_ALWAYS) {
            if(overwrite === that.OVERWRITE_NEVER) {
                result.action = false;
                deferred.resolve(result);
            } else if(overwrite === that.OVERWRITE_ASK) {
                read(
                    {
                        prompt: 'Overwrite : <' + toFile.origin + '>/' + toFile.path +
                            ' (' + toFile.lastModified + ') with <' + fromFile.origin + '>/' +
                            fromFile.path +
                            ' (' + fromFile.lastModified + ')? Y/[N]',
                        'default': 'N'
                    },
                    function(er, value) {
                        if(er) {
                            deferred.reject(er);
                        } else if(value !== undefined) {
                            result.action = (value.toUpperCase() === 'Y');
                            deferred.resolve(result);
                        } else {
                            deferred.reject();
                        }
                    }
                );
            } else if(overwrite === that.OVERWRITE_IF_NEWER){
                result.action = (
                    new Date(fromFile.lastModified).getTime() >
                    new Date(toFile.lastModified).getTime()
                );
                deferred.resolve(result);
            }
        } else {
            deferred.resolve(result);
        }
        return deferred.promise;
    };

    var sync = function(project, from, to, overwrite, deleteOrphaned, simulate, fromOrigin, toOrigin){
        var asyncPromises = [];
        var sequentialPromises = [];

        Object.keys(from).forEach(function(file){
            var fromFile = {
                path: file,
                exists: true,
                lastModified: from[file].lastModified,
                origin: fromOrigin
            };

            var toFile  = {
                path: file,
                exists: to[file] !== undefined,
                lastModified: to[file] ? to[file].lastModified : undefined,
                origin: toOrigin
            };

            var promise = function() {
                var deferred = Q.defer();
                performAction(fromFile, toFile, overwrite).then(function(result){
                    if(result.action) {
                        console.log((fromOrigin === 'remote' ? 'Download ' : 'Upload ') + result.fromFile.path);
                        if(!simulate) {
                            (
                                fromOrigin === 'remote' ?
                                writeToLocalFile(project, result.fromFile.path) :
                                writeToRemoteFile(project, result.fromFile.path)
                            )
                            .then(function(){
                                deferred.resolve();
                            })
                            .catch(function(error){
                                deferred.reject(error);
                            });
                        } else {
                            deferred.resolve();
                        }
                    } else {
                        deferred.resolve();
                    }
                })
                .catch(function(error){
                    console.error(error);
                });
                return deferred.promise;
            };
            if(toFile.exists === true && overwrite === that.OVERWRITE_ASK) {
                sequentialPromises.push(promise);
            } else {
                asyncPromises.push(promise());
            }
        });

        if(deleteOrphaned) {
            Object.keys(to).forEach(function(file){
                if(!from[file]) {
                    asyncPromises.push(function(){
                        var deferred = Q.defer();
                        console.log('Delete ' + '<' + toOrigin + '>/' + file);
                        if(!simulate) {
                            (
                                fromOrigin === 'remote' ?
                                deleteLocalFile(file) :
                                deleteRemoteFile(project, file)
                            )
                            .then(function(){
                                deferred.resolve();
                            })
                            .catch(function(error){
                                deferred.reject(error);
                            });
                        } else {
                            deferred.resolve();
                        }
                        return deferred.promise;
                    });
                }
            });
        }

        sequentialPromises.push(function(){ return Q.all(asyncPromises); });
        return sequentialPromises.reduce(Q.when, Q());
    };

    var filterList = function(list, ignore) {
        Object.keys(list).forEach(function(path){
            //Delete public or private files that don't have a .jq or .xq suffix
            if(
                (path.indexOf('public/') === 0 || path.indexOf('private/') === 0) &&
                path.substring(path.length - 3) !== '.jq' &&
                path.substring(path.length - 3) !== '.xq'
            ) {
                delete list[path];
            } else {
                var matched = false;
                ignore.forEach(function(mask){
                    if(path.match(mask) !== null) {
                        matched = true;
                        return false;
                    }
                });
                if(matched === true) {
                    delete list[path];
                }
            }
        });
        return list;
    };

    this.upload = function(project, remote, local, overwrite, deleteOrphaned, simulate, ignore){
        remote = filterList(remote, ignore);
        local  = filterList(local, ignore);
        return sync(
            project, local, remote, overwrite,
            deleteOrphaned, simulate, 'local', 'remote'
        );
    };

    this.download = function(project, remote, local, overwrite, deleteOrphaned, simulate, ignore){
        remote = filterList(remote, ignore);
        local  = filterList(local, ignore);
        return sync(
            project, remote, local, overwrite,
            deleteOrphaned, simulate, 'remote', 'local'
        );
    };

    this.truncate = function(project, remote, simulate, ignore){
        remote = filterList(remote, ignore);
        return sync(
            project, {}, remote, this.OVERWRITE_ALWAYS,
            true, simulate, 'local', 'remote'
        );
    };
};

module.exports = new Client();
<|endoftext|>


<commit_before>// Dependencies
var Response = require('./response.js')
var Call     = require('./call.js')
var proto    = require('./proto.js')
var retry    = require('./retry.js')

// TxnRetryOptions sets the retry options for handling write conflicts.
var TxnRetryOptions = new retry.Options({
	backoff:     50, // ms
	maxBackoff:  5000,
	constant:    2,
	maxAttempts: 0, // retry indefinitely
})

// Returns you a KV rpc client
// @constructor
// @param {sender} - http request client
// @param {clock}  - clock module
function KV(sender, clock) {
	// User is the default user to set on API calls. If User is set to
	// non-empty in call arguments, this value is ignored.
	this.user = 'root'
	// UserPriority is the default user priority to set on API calls. If
	// UserPriority is set non-zero in call arguments, this value is
	// ignored.
	this.userPriority = 0 // int32

	this.sender = sender
	this.clock  = clock

	this.preparedBuffer = []

	return this
}

// Sender returns the sender supplied to NewKV, unless wrapped by a
// transactional sender, in which case returns the unwrapped sender.
KV.prototype.Sender = function Sender() {
	if(this.sender instanceof TxnSender) {
		return this.wrapped
	}
	else {
		return this.sender
	}
}

// Call invokes the KV command synchronously and returns the response
// and error, if applicable. If preceeding calls have been made to
// Prepare() without a call to Flush(), this call is prepared and
// then all prepared calls are flushed.
KV.prototype.Call = function Call(method, request, callback) {
	if(! request.get('header.user')) {
		request.set('header.user', this.user)
	}
	if(this.user_priority != 0 && ! request.get('header.user_priority')) {
		request.set('header.user_priority', this.user_priority)
	}

	var call = new Call({
		Method: method,
		Request: request,
	})
	call.resetClientCmdID(null) // TODO: set apropieate clock module
	this.sender.Send(call, function(err, res) {
		if(err) {
			console.log('returning generic error')
			callback(err, null)
			return
		}

		var error = res.get('header.error')

		if(error) {
			console.log('returnng error from header')
			callback(error, res)
			return
		}

		callback(null, res)
	})
}

// Prepare accepts a KV API call, specified by method name, arguments
// and a reply struct. The call will be buffered locally until the
// first call to Flush(), at which time it will be sent for execution
// as part of a batch call. Using Prepare/Flush parallelizes queries
// and updates and should be used where possible for efficiency.
//
// For clients using an HTTP sender, Prepare/Flush allows multiple
// commands to be sent over the same connection. For transactional
// clients, Prepare/Flush can dramatically improve efficiency by
// compressing multiple writes into a single atomic update in the
// event that the writes are to keys within a single range. However,
// using Prepare/Flush alone will not guarantee atomicity. Clients
// must use a transaction for that purpose.
//
// The supplied reply struct will not be valid until after a call
// to Flush().
KV.prototype.Prepare = function Prepare(method, request, callback) {
	// Make callback optional
	if(typeof callback == 'undefined') {
		callback = empty_callback
	}

	// Validate callback
	if(callback instanceof Function) {
		this.prepareBuffer.push({
			Method: method,
			Request: request,
			Callback: callback,
		})
	}
	// Put the callback response in a response object
	// Replace the callback with a response object
	else if(callback instanceof Response) {
		var response = callback
		// TODO: Replace this function call, don't even callback
		// Write to the response object at once at .flush()
		var updateResponse = function CallbackPreparedResponse(err, res) {
			response.err = err
			response.res = res
		}

		this.prepareBuffer.push({
			Method: method,
			Request: request,
			Callback: updateResponse,
		})
	}
	// Not a valid callback
	else {
		throw new Error('invalid callback, check your code! received a: ' + typeof callback)
	}
}

// Flush sends all previously prepared calls, buffered by invocations
// of Prepare(). The calls are organized into a single batch command
// and sent together. Flush returns nil if all prepared calls are
// executed successfully. Otherwise, Flush returns the first error,
// where calls are executed in the order in which they were prepared.
// After Flush returns, all prepared reply structs will be valid.
KV.prototype.Flush = function Flush(callback) {
	if(! callback) {
		callback = empty_callback
	}

	if(this.prepareBuffer.length === 0) {
		callback(null)
		return
	}
	else if (this.prepareBuffer.length === 1) {
		var call = this.prepareBuffer[0]
		this.Call(call.Method, call.Request, call.Callback)
		this.prepareBuffer = []
		return
	}

	var batch = []
	var cbs   = []

	for(var i in this.prepareBuffer) {
		batch[i] = this.prepareBuffer[i].Request
		cbs[i]   = this.prepareBuffer[i].Callback
	}

	// Clear buffer
	this.prepareBuffer = []

	// Make call
	this.Call(proto.Batch, new proto.BatchRequest(batch), function FlushCallback(err, res) {
		if(err) {
			callback(err)
			return
		}

		for(var i in res.responses) {
			var resp = res.responses[i]

			if(resp.error) {
				cbs[i](resp.error, resp)
				return
			}

			cbs[i](null, resp)
		}

		callback(null)
	})
}

// RunTransaction executes retryable in the context of a distributed
// transaction. The transaction is automatically aborted if retryable
// returns any error aside from recoverable internal errors, and is
// automatically committed otherwise. retryable should have no side
// effects which could cause problems in the event it must be run more
// than once. The opts struct contains transaction settings.
//
// Calling RunTransaction on the transactional KV client which is
// supplied to the retryable function is an error.
KV.prototype.RunTransaction = function RunTransaction(opts, retryable, callback) {
	if(this.sender instanceof TxnSender) {
		throw new Error('cannot invoke RunTransaction on an already-transactional client')
	}

	// Create a new KV for the transaction using a transactional KV sender.
	var txnSender = new TxnSender(this.sender, {
		name: opts.name || '',
		isolation: opts.isolation || 0,
	})

	var txnKV = new KV(txnSender, this.clock)
	txnKV.user = this.user
	txnKV.user_priority = this.user_priority

	// Run retryable in a retry loop until we encounter a success or
	// error condition this loop isn't capable of handling.
	var retryOpts =	TxnRetryOptions
	retryOpts.tag = opts.name
	retry.WithBackoff(retryOpts, function Retry(callback) {
		txnSender.txnEnd = false // always reset before [re]starting txn

		// User is trying to commit the trasaction
		var commit = function Commit() {
			if(txnSender.txnEnd === false) {
				var request = new proto.EndTransactionRequest(true)

				txnKV.Prepare(proto.EndTransaction, request, function txnResponseCallback(err, res) {
					callback(err, res)
				})

				txnKV.Flush(function(err) {
					if(err instanceof proto.ReadWithinUncertaintyIntervalError) {
						// Retry immediately on read within uncertainty interval.
						callback(null, retry.Reset)
					}
					else if(err instanceof proto.TransactionAbortedError) {
						// If the transaction was aborted, the txnSender will have created
						// a new txn. We allow backoff/retry in this case.
						callback(null, retry.Continue)
					}
					else if(err instanceof proto.TransactionPushError) {
						// Backoff and retry on failure to push a conflicting transaction.
						callback(null, retry.Continue)
					}
					else if(err instanceof proto.TransactionRetryError) {
						// Return RetryReset for an immediate retry (as in the case of
						// an SSI txn whose timestamp was pushed).
						callback(null, retry.Reset)
					}
					else {
						// For all other cases, finish retry loop, returning possible error.
						callback(err, retry.Break)
					}
				})
			}
		}
		// Something when wrong, user is aborting
		var abort = function Abort(err) {
			kvClient.Close()
			txnSender.txnEnd = true

			err = err || new Error('transacation aborted by user, no error was specified.')

			callback(err, retry.Break)
		}

		// Run retryable transaction fnc
		try {
			retryable(txnKV, commit, abort)
		} catch(e) {
			// Abort the transaction
			abort(e)
		}
	}, function Finish(err) {
		if(err && txnSender.txnEnd === false) {
			var request = proto.EndTransactionRequest(false)
			var txnErr = err

			txnKV.Call(proto.EndTransaction, request, function(err, res) {
				if(err) {
					console.log('failure aborting transaction: %s; abort caused by: %s', res.error, txnErr)
				}

				callback(err, res)
			})
			return
		}

		callback(err)
	})
}

// Close sender
KV.prototype.Close = function Close() {
	this.sender.Close()
}

// Helper fnc, reference to an empty callback
function empty_cabllack() {}

// Exports
module.exports = KV
<commit_msg>renamed function, was confliction with module name<commit_after>// Dependencies
var Response = require('./response.js')
var Call     = require('./call.js')
var proto    = require('./proto.js')
var retry    = require('./retry.js')

// TxnRetryOptions sets the retry options for handling write conflicts.
var TxnRetryOptions = new retry.Options({
	backoff:     50, // ms
	maxBackoff:  5000,
	constant:    2,
	maxAttempts: 0, // retry indefinitely
})

// Returns you a KV rpc client
// @constructor
// @param {sender} - http request client
// @param {clock}  - clock module
function KV(sender, clock) {
	// User is the default user to set on API calls. If User is set to
	// non-empty in call arguments, this value is ignored.
	this.user = 'root'
	// UserPriority is the default user priority to set on API calls. If
	// UserPriority is set non-zero in call arguments, this value is
	// ignored.
	this.userPriority = 0 // int32

	this.sender = sender
	this.clock  = clock

	this.preparedBuffer = []

	return this
}

// Sender returns the sender supplied to NewKV, unless wrapped by a
// transactional sender, in which case returns the unwrapped sender.
KV.prototype.Sender = function Sender() {
	if(this.sender instanceof TxnSender) {
		return this.wrapped
	}
	else {
		return this.sender
	}
}

// Call invokes the KV command synchronously and returns the response
// and error, if applicable. If preceeding calls have been made to
// Prepare() without a call to Flush(), this call is prepared and
// then all prepared calls are flushed.
KV.prototype.Call = function KVCall(method, request, callback) {
	console.log(1)
	var header = request.get('header')

	if(! header.get('user')) {
		header.set('user', this.user)
	}
	if(this.userPriority > 0 && ! header.get('user_priority')) {
		header.set('user_priority', this.userPriority)
	}

	var call = new Call({
		Method: method,
		Request: request,
	})
	call.resetClientCmdID(null) // TODO: set apropieate clock module
	this.sender.Send(call, function(err, res) {
		if(err) {
			console.log('returning generic error')
			callback(err, null)
			return
		}

		var error = res.get('header.error')

		if(error) {
			console.log('returnng error from header')
			callback(error, res)
			return
		}

		callback(null, res)
	})
}

// Prepare accepts a KV API call, specified by method name, arguments
// and a reply struct. The call will be buffered locally until the
// first call to Flush(), at which time it will be sent for execution
// as part of a batch call. Using Prepare/Flush parallelizes queries
// and updates and should be used where possible for efficiency.
//
// For clients using an HTTP sender, Prepare/Flush allows multiple
// commands to be sent over the same connection. For transactional
// clients, Prepare/Flush can dramatically improve efficiency by
// compressing multiple writes into a single atomic update in the
// event that the writes are to keys within a single range. However,
// using Prepare/Flush alone will not guarantee atomicity. Clients
// must use a transaction for that purpose.
//
// The supplied reply struct will not be valid until after a call
// to Flush().
KV.prototype.Prepare = function Prepare(method, request, callback) {
	// Make callback optional
	if(typeof callback == 'undefined') {
		callback = empty_callback
	}

	// Validate callback
	if(callback instanceof Function) {
		this.prepareBuffer.push({
			Method: method,
			Request: request,
			Callback: callback,
		})
	}
	// Put the callback response in a response object
	// Replace the callback with a response object
	else if(callback instanceof Response) {
		var response = callback
		// TODO: Replace this function call, don't even callback
		// Write to the response object at once at .flush()
		var updateResponse = function CallbackPreparedResponse(err, res) {
			response.err = err
			response.res = res
		}

		this.prepareBuffer.push({
			Method: method,
			Request: request,
			Callback: updateResponse,
		})
	}
	// Not a valid callback
	else {
		throw new Error('invalid callback, check your code! received a: ' + typeof callback)
	}
}

// Flush sends all previously prepared calls, buffered by invocations
// of Prepare(). The calls are organized into a single batch command
// and sent together. Flush returns nil if all prepared calls are
// executed successfully. Otherwise, Flush returns the first error,
// where calls are executed in the order in which they were prepared.
// After Flush returns, all prepared reply structs will be valid.
KV.prototype.Flush = function Flush(callback) {
	if(! callback) {
		callback = empty_callback
	}

	if(this.prepareBuffer.length === 0) {
		callback(null)
		return
	}
	else if (this.prepareBuffer.length === 1) {
		var call = this.prepareBuffer[0]
		this.Call(call.Method, call.Request, call.Callback)
		this.prepareBuffer = []
		return
	}

	var batch = []
	var cbs   = []

	for(var i in this.prepareBuffer) {
		batch[i] = this.prepareBuffer[i].Request
		cbs[i]   = this.prepareBuffer[i].Callback
	}

	// Clear buffer
	this.prepareBuffer = []

	// Make call
	this.Call(proto.Batch, new proto.BatchRequest(batch), function FlushCallback(err, res) {
		if(err) {
			callback(err)
			return
		}

		for(var i in res.responses) {
			var resp = res.responses[i]

			if(resp.error) {
				cbs[i](resp.error, resp)
				return
			}

			cbs[i](null, resp)
		}

		callback(null)
	})
}

// RunTransaction executes retryable in the context of a distributed
// transaction. The transaction is automatically aborted if retryable
// returns any error aside from recoverable internal errors, and is
// automatically committed otherwise. retryable should have no side
// effects which could cause problems in the event it must be run more
// than once. The opts struct contains transaction settings.
//
// Calling RunTransaction on the transactional KV client which is
// supplied to the retryable function is an error.
KV.prototype.RunTransaction = function RunTransaction(opts, retryable, callback) {
	if(this.sender instanceof TxnSender) {
		throw new Error('cannot invoke RunTransaction on an already-transactional client')
	}

	// Create a new KV for the transaction using a transactional KV sender.
	var txnSender = new TxnSender(this.sender, {
		name: opts.name || '',
		isolation: opts.isolation || 0,
	})

	var txnKV = new KV(txnSender, this.clock)
	txnKV.user = this.user
	txnKV.userPriority = this.userPriority

	// Run retryable in a retry loop until we encounter a success or
	// error condition this loop isn't capable of handling.
	var retryOpts =	TxnRetryOptions
	retryOpts.tag = opts.name
	retry.WithBackoff(retryOpts, function Retry(callback) {
		txnSender.txnEnd = false // always reset before [re]starting txn

		// User is trying to commit the trasaction
		var commit = function Commit() {
			if(txnSender.txnEnd === false) {
				var request = new proto.EndTransactionRequest(true)

				txnKV.Prepare(proto.EndTransaction, request, function txnResponseCallback(err, res) {
					callback(err, res)
				})

				txnKV.Flush(function(err) {
					if(err instanceof proto.ReadWithinUncertaintyIntervalError) {
						// Retry immediately on read within uncertainty interval.
						callback(null, retry.Reset)
					}
					else if(err instanceof proto.TransactionAbortedError) {
						// If the transaction was aborted, the txnSender will have created
						// a new txn. We allow backoff/retry in this case.
						callback(null, retry.Continue)
					}
					else if(err instanceof proto.TransactionPushError) {
						// Backoff and retry on failure to push a conflicting transaction.
						callback(null, retry.Continue)
					}
					else if(err instanceof proto.TransactionRetryError) {
						// Return RetryReset for an immediate retry (as in the case of
						// an SSI txn whose timestamp was pushed).
						callback(null, retry.Reset)
					}
					else {
						// For all other cases, finish retry loop, returning possible error.
						callback(err, retry.Break)
					}
				})
			}
		}
		// Something when wrong, user is aborting
		var abort = function Abort(err) {
			kvClient.Close()
			txnSender.txnEnd = true

			err = err || new Error('transacation aborted by user, no error was specified.')

			callback(err, retry.Break)
		}

		// Run retryable transaction fnc
		try {
			retryable(txnKV, commit, abort)
		} catch(e) {
			// Abort the transaction
			abort(e)
		}
	}, function Finish(err) {
		if(err && txnSender.txnEnd === false) {
			var request = proto.EndTransactionRequest(false)
			var txnErr = err

			txnKV.Call(proto.EndTransaction, request, function(err, res) {
				if(err) {
					console.log('failure aborting transaction: %s; abort caused by: %s', res.error, txnErr)
				}

				callback(err, res)
			})
			return
		}

		callback(err)
	})
}

// Close sender
KV.prototype.Close = function Close() {
	this.sender.Close()
}

// Helper fnc, reference to an empty callback
function empty_cabllack() {}

// Exports
module.exports = KV
<|endoftext|>


<commit_before>/*!
 * ws: a node.js websocket client
 * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
 * MIT Licensed
 */

'use strict';

const safeBuffer = require('safe-buffer');
const crypto = require('crypto');

const PerMessageDeflate = require('./PerMessageDeflate');
const bufferUtil = require('./BufferUtil');
const ErrorCodes = require('./ErrorCodes');

const Buffer = safeBuffer.Buffer;

/**
 * HyBi Sender implementation.
 */
class Sender {
  /**
   * Creates a Sender instance.
   *
   * @param {net.Socket} socket The connection socket
   * @param {Object} extensions An object containing the negotiated extensions
   */
  constructor (socket, extensions) {
    this.perMessageDeflate = (extensions || {})[PerMessageDeflate.extensionName];
    this._socket = socket;

    this.firstFragment = true;
    this.compress = false;

    this.bufferedBytes = 0;
    this.deflating = false;
    this.queue = [];

    this.onerror = null;
  }

  /**
   * Frames a piece of data according to the HyBi WebSocket protocol.
   *
   * @param {Buffer} data The data to frame
   * @param {Object} options Options object
   * @param {Number} options.opcode The opcode
   * @param {Boolean} options.readOnly Specifies whether `data` can be modified
   * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
   * @param {Boolean} options.mask Specifies whether or not to mask `data`
   * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
   * @return {Buffer[]} The framed data as a list of `Buffer` instances
   * @public
   */
  static frame (data, options) {
    const merge = data.length < 1024 || (options.mask && options.readOnly);
    var offset = options.mask ? 6 : 2;
    var payloadLength = data.length;

    if (data.length >= 65536) {
      offset += 8;
      payloadLength = 127;
    } else if (data.length > 125) {
      offset += 2;
      payloadLength = 126;
    }

    const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);

    target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
    if (options.rsv1) target[0] |= 0x40;

    if (payloadLength === 126) {
      target.writeUInt16BE(data.length, 2, true);
    } else if (payloadLength === 127) {
      target.writeUInt32BE(0, 2, true);
      target.writeUInt32BE(data.length, 6, true);
    }

    if (!options.mask) {
      target[1] = payloadLength;
      if (merge) {
        data.copy(target, offset);
        return [target];
      }

      return [target, data];
    }

    const mask = crypto.randomBytes(4);

    target[1] = payloadLength | 0x80;
    target[offset - 4] = mask[0];
    target[offset - 3] = mask[1];
    target[offset - 2] = mask[2];
    target[offset - 1] = mask[3];

    if (merge) {
      bufferUtil.mask(data, mask, target, offset, data.length);
      return [target];
    }

    bufferUtil.mask(data, mask, data, 0, data.length);
    return [target, data];
  }

  /**
   * Sends a close message to the other peer.
   *
   * @param {(Number|undefined)} code The status code component of the body
   * @param {String} data The message component of the body
   * @param {Boolean} mask Specifies whether or not to mask the message
   * @param {Function} cb Callback
   * @public
   */
  close (code, data, mask, cb) {
    if (code !== undefined && (typeof code !== 'number' || !ErrorCodes.isValidErrorCode(code))) {
      throw new Error('first argument must be a valid error code number');
    }

    const buf = Buffer.allocUnsafe(2 + (data ? Buffer.byteLength(data) : 0));

    buf.writeUInt16BE(code || 1000, 0, true);
    if (buf.length > 2) buf.write(data, 2);

    if (this.deflating) {
      this.enqueue([this.doClose, buf, mask, cb]);
    } else {
      this.doClose(buf, mask, cb);
    }
  }

  /**
   * Frames and sends a close message.
   *
   * @param {Buffer} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @param {Function} cb Callback
   * @private
   */
  doClose (data, mask, cb) {
    this.sendFrame(Sender.frame(data, {
      readOnly: false,
      opcode: 0x08,
      rsv1: false,
      fin: true,
      mask
    }), cb);
  }

  /**
   * Sends a ping message to the other peer.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @public
   */
  ping (data, mask) {
    var readOnly = true;

    if (!Buffer.isBuffer(data)) {
      if (data instanceof ArrayBuffer) {
        data = Buffer.from(data);
      } else if (ArrayBuffer.isView(data)) {
        data = viewToBuffer(data);
      } else {
        data = Buffer.from(data);
        readOnly = false;
      }
    }

    if (this.deflating) {
      this.enqueue([this.doPing, data, mask, readOnly]);
    } else {
      this.doPing(data, mask, readOnly);
    }
  }

  /**
   * Frames and sends a ping message.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @param {Boolean} readOnly Specifies whether `data` can be modified
   * @private
   */
  doPing (data, mask, readOnly) {
    this.sendFrame(Sender.frame(data, {
      opcode: 0x09,
      rsv1: false,
      fin: true,
      readOnly,
      mask
    }));
  }

  /**
   * Sends a pong message to the other peer.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @public
   */
  pong (data, mask) {
    var readOnly = true;

    if (!Buffer.isBuffer(data)) {
      if (data instanceof ArrayBuffer) {
        data = Buffer.from(data);
      } else if (ArrayBuffer.isView(data)) {
        data = viewToBuffer(data);
      } else {
        data = Buffer.from(data);
        readOnly = false;
      }
    }

    if (this.deflating) {
      this.enqueue([this.doPong, data, mask, readOnly]);
    } else {
      this.doPong(data, mask, readOnly);
    }
  }

  /**
   * Frames and sends a pong message.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @param {Boolean} readOnly Specifies whether `data` can be modified
   * @private
   */
  doPong (data, mask, readOnly) {
    this.sendFrame(Sender.frame(data, {
      opcode: 0x0a,
      rsv1: false,
      fin: true,
      readOnly,
      mask
    }));
  }

  /**
   * Sends a data message to the other peer.
   *
   * @param {*} data The message to send
   * @param {Object} options Options object
   * @param {Boolean} options.compress Specifies whether or not to compress `data`
   * @param {Boolean} options.binary Specifies whether `data` is binary or text
   * @param {Boolean} options.fin Specifies whether the fragment is the last one
   * @param {Boolean} options.mask Specifies whether or not to mask `data`
   * @param {Function} cb Callback
   * @public
   */
  send (data, options, cb) {
    var opcode = options.binary ? 2 : 1;
    var rsv1 = options.compress;
    var readOnly = true;

    if (!Buffer.isBuffer(data)) {
      if (data instanceof ArrayBuffer) {
        data = Buffer.from(data);
      } else if (ArrayBuffer.isView(data)) {
        data = viewToBuffer(data);
      } else {
        data = Buffer.from(data);
        readOnly = false;
      }
    }

    if (this.firstFragment) {
      this.firstFragment = false;
      if (rsv1 && this.perMessageDeflate) {
        rsv1 = data.length >= this.perMessageDeflate.threshold;
      }
      this.compress = rsv1;
    } else {
      rsv1 = false;
      opcode = 0;
    }

    if (options.fin) this.firstFragment = true;

    if (this.perMessageDeflate) {
      const opts = {
        compress: this.compress,
        mask: options.mask,
        fin: options.fin,
        readOnly,
        opcode,
        rsv1
      };

      if (this.deflating) {
        this.enqueue([this.dispatch, data, opts, cb]);
      } else {
        this.dispatch(data, opts, cb);
      }
    } else {
      this.sendFrame(Sender.frame(data, {
        mask: options.mask,
        fin: options.fin,
        rsv1: false,
        readOnly,
        opcode
      }), cb);
    }
  }

  /**
   * Dispatches a data message.
   *
   * @param {Buffer} data The message to send
   * @param {Object} options Options object
   * @param {Number} options.opcode The opcode
   * @param {Boolean} options.readOnly Specifies whether `data` can be modified
   * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
   * @param {Boolean} options.compress Specifies whether or not to compress `data`
   * @param {Boolean} options.mask Specifies whether or not to mask `data`
   * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
   * @param {Function} cb Callback
   * @private
   */
  dispatch (data, options, cb) {
    if (!options.compress) {
      this.sendFrame(Sender.frame(data, options), cb);
      return;
    }

    this.deflating = true;
    this.perMessageDeflate.compress(data, options.fin, (err, buf) => {
      if (err) {
        if (cb) cb(err);
        else this.onerror(err);
        return;
      }

      options.readOnly = false;
      this.sendFrame(Sender.frame(buf, options), cb);
      this.deflating = false;
      this.dequeue();
    });
  }

  /**
   * Executes queued send operations.
   *
   * @private
   */
  dequeue () {
    while (!this.deflating && this.queue.length) {
      const params = this.queue.shift();

      this.bufferedBytes -= params[1].length;
      params[0].apply(this, params.slice(1));
    }
  }

  /**
   * Enqueues a send operation.
   *
   * @param {Array} params Send operation parameters.
   * @private
   */
  enqueue (params) {
    this.bufferedBytes += params[1].length;
    this.queue.push(params);
  }

  /**
   * Sends a frame.
   *
   * @param {Buffer[]} list The frame to send
   * @param {Function} cb Callback
   * @private
   */
  sendFrame (list, cb) {
    if (list.length === 2) {
      this._socket.write(list[0]);
      this._socket.write(list[1], cb);
    } else {
      this._socket.write(list[0], cb);
    }
  }
}

module.exports = Sender;

/**
 * Converts an `ArrayBuffer` view into a buffer.
 *
 * @param {(DataView|TypedArray)} view The view to convert
 * @return {Buffer} Converted view
 * @private
 */
function viewToBuffer (view) {
  const buf = Buffer.from(view.buffer);

  if (view.byteLength !== view.buffer.byteLength) {
    return buf.slice(view.byteOffset, view.byteOffset + view.byteLength);
  }

  return buf;
}
<commit_msg>[perf] Prevent `Sender.frame()` from being deoptimized<commit_after>/*!
 * ws: a node.js websocket client
 * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
 * MIT Licensed
 */

'use strict';

const safeBuffer = require('safe-buffer');
const crypto = require('crypto');

const PerMessageDeflate = require('./PerMessageDeflate');
const bufferUtil = require('./BufferUtil');
const ErrorCodes = require('./ErrorCodes');

const Buffer = safeBuffer.Buffer;

/**
 * HyBi Sender implementation.
 */
class Sender {
  /**
   * Creates a Sender instance.
   *
   * @param {net.Socket} socket The connection socket
   * @param {Object} extensions An object containing the negotiated extensions
   */
  constructor (socket, extensions) {
    this.perMessageDeflate = (extensions || {})[PerMessageDeflate.extensionName];
    this._socket = socket;

    this.firstFragment = true;
    this.compress = false;

    this.bufferedBytes = 0;
    this.deflating = false;
    this.queue = [];

    this.onerror = null;
  }

  /**
   * Frames a piece of data according to the HyBi WebSocket protocol.
   *
   * @param {Buffer} data The data to frame
   * @param {Object} options Options object
   * @param {Number} options.opcode The opcode
   * @param {Boolean} options.readOnly Specifies whether `data` can be modified
   * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
   * @param {Boolean} options.mask Specifies whether or not to mask `data`
   * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
   * @return {Buffer[]} The framed data as a list of `Buffer` instances
   * @public
   */
  static frame (data, options) {
    const merge = data.length < 1024 || (options.mask && options.readOnly);
    var offset = options.mask ? 6 : 2;
    var payloadLength = data.length;

    if (data.length >= 65536) {
      offset += 8;
      payloadLength = 127;
    } else if (data.length > 125) {
      offset += 2;
      payloadLength = 126;
    }

    const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);

    target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
    if (options.rsv1) target[0] |= 0x40;

    if (payloadLength === 126) {
      target.writeUInt16BE(data.length, 2, true);
    } else if (payloadLength === 127) {
      target.writeUInt32BE(0, 2, true);
      target.writeUInt32BE(data.length, 6, true);
    }

    if (!options.mask) {
      target[1] = payloadLength;
      if (merge) {
        data.copy(target, offset);
        return [target];
      }

      return [target, data];
    }

    const mask = crypto.randomBytes(4);

    target[1] = payloadLength | 0x80;
    target[offset - 4] = mask[0];
    target[offset - 3] = mask[1];
    target[offset - 2] = mask[2];
    target[offset - 1] = mask[3];

    if (merge) {
      bufferUtil.mask(data, mask, target, offset, data.length);
      return [target];
    }

    bufferUtil.mask(data, mask, data, 0, data.length);
    return [target, data];
  }

  /**
   * Sends a close message to the other peer.
   *
   * @param {(Number|undefined)} code The status code component of the body
   * @param {String} data The message component of the body
   * @param {Boolean} mask Specifies whether or not to mask the message
   * @param {Function} cb Callback
   * @public
   */
  close (code, data, mask, cb) {
    if (code !== undefined && (typeof code !== 'number' || !ErrorCodes.isValidErrorCode(code))) {
      throw new Error('first argument must be a valid error code number');
    }

    const buf = Buffer.allocUnsafe(2 + (data ? Buffer.byteLength(data) : 0));

    buf.writeUInt16BE(code || 1000, 0, true);
    if (buf.length > 2) buf.write(data, 2);

    if (this.deflating) {
      this.enqueue([this.doClose, buf, mask, cb]);
    } else {
      this.doClose(buf, mask, cb);
    }
  }

  /**
   * Frames and sends a close message.
   *
   * @param {Buffer} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @param {Function} cb Callback
   * @private
   */
  doClose (data, mask, cb) {
    this.sendFrame(Sender.frame(data, {
      fin: true,
      rsv1: false,
      opcode: 0x08,
      mask,
      readOnly: false
    }), cb);
  }

  /**
   * Sends a ping message to the other peer.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @public
   */
  ping (data, mask) {
    var readOnly = true;

    if (!Buffer.isBuffer(data)) {
      if (data instanceof ArrayBuffer) {
        data = Buffer.from(data);
      } else if (ArrayBuffer.isView(data)) {
        data = viewToBuffer(data);
      } else {
        data = Buffer.from(data);
        readOnly = false;
      }
    }

    if (this.deflating) {
      this.enqueue([this.doPing, data, mask, readOnly]);
    } else {
      this.doPing(data, mask, readOnly);
    }
  }

  /**
   * Frames and sends a ping message.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @param {Boolean} readOnly Specifies whether `data` can be modified
   * @private
   */
  doPing (data, mask, readOnly) {
    this.sendFrame(Sender.frame(data, {
      fin: true,
      rsv1: false,
      opcode: 0x09,
      mask,
      readOnly
    }));
  }

  /**
   * Sends a pong message to the other peer.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @public
   */
  pong (data, mask) {
    var readOnly = true;

    if (!Buffer.isBuffer(data)) {
      if (data instanceof ArrayBuffer) {
        data = Buffer.from(data);
      } else if (ArrayBuffer.isView(data)) {
        data = viewToBuffer(data);
      } else {
        data = Buffer.from(data);
        readOnly = false;
      }
    }

    if (this.deflating) {
      this.enqueue([this.doPong, data, mask, readOnly]);
    } else {
      this.doPong(data, mask, readOnly);
    }
  }

  /**
   * Frames and sends a pong message.
   *
   * @param {*} data The message to send
   * @param {Boolean} mask Specifies whether or not to mask `data`
   * @param {Boolean} readOnly Specifies whether `data` can be modified
   * @private
   */
  doPong (data, mask, readOnly) {
    this.sendFrame(Sender.frame(data, {
      fin: true,
      rsv1: false,
      opcode: 0x0a,
      mask,
      readOnly
    }));
  }

  /**
   * Sends a data message to the other peer.
   *
   * @param {*} data The message to send
   * @param {Object} options Options object
   * @param {Boolean} options.compress Specifies whether or not to compress `data`
   * @param {Boolean} options.binary Specifies whether `data` is binary or text
   * @param {Boolean} options.fin Specifies whether the fragment is the last one
   * @param {Boolean} options.mask Specifies whether or not to mask `data`
   * @param {Function} cb Callback
   * @public
   */
  send (data, options, cb) {
    var opcode = options.binary ? 2 : 1;
    var rsv1 = options.compress;
    var readOnly = true;

    if (!Buffer.isBuffer(data)) {
      if (data instanceof ArrayBuffer) {
        data = Buffer.from(data);
      } else if (ArrayBuffer.isView(data)) {
        data = viewToBuffer(data);
      } else {
        data = Buffer.from(data);
        readOnly = false;
      }
    }

    if (this.firstFragment) {
      this.firstFragment = false;
      if (rsv1 && this.perMessageDeflate) {
        rsv1 = data.length >= this.perMessageDeflate.threshold;
      }
      this.compress = rsv1;
    } else {
      rsv1 = false;
      opcode = 0;
    }

    if (options.fin) this.firstFragment = true;

    if (this.perMessageDeflate) {
      const opts = {
        fin: options.fin,
        rsv1,
        opcode,
        mask: options.mask,
        readOnly
      };

      if (this.deflating) {
        this.enqueue([this.dispatch, data, this.compress, opts, cb]);
      } else {
        this.dispatch(data, this.compress, opts, cb);
      }
    } else {
      this.sendFrame(Sender.frame(data, {
        fin: options.fin,
        rsv1: false,
        opcode,
        mask: options.mask,
        readOnly
      }), cb);
    }
  }

  /**
   * Dispatches a data message.
   *
   * @param {Buffer} data The message to send
   * @param {Boolean} compress Specifies whether or not to compress `data`
   * @param {Object} options Options object
   * @param {Number} options.opcode The opcode
   * @param {Boolean} options.readOnly Specifies whether `data` can be modified
   * @param {Boolean} options.fin Specifies whether or not to set the FIN bit
   * @param {Boolean} options.mask Specifies whether or not to mask `data`
   * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit
   * @param {Function} cb Callback
   * @private
   */
  dispatch (data, compress, options, cb) {
    if (!compress) {
      this.sendFrame(Sender.frame(data, options), cb);
      return;
    }

    this.deflating = true;
    this.perMessageDeflate.compress(data, options.fin, (err, buf) => {
      if (err) {
        if (cb) cb(err);
        else this.onerror(err);
        return;
      }

      options.readOnly = false;
      this.sendFrame(Sender.frame(buf, options), cb);
      this.deflating = false;
      this.dequeue();
    });
  }

  /**
   * Executes queued send operations.
   *
   * @private
   */
  dequeue () {
    while (!this.deflating && this.queue.length) {
      const params = this.queue.shift();

      this.bufferedBytes -= params[1].length;
      params[0].apply(this, params.slice(1));
    }
  }

  /**
   * Enqueues a send operation.
   *
   * @param {Array} params Send operation parameters.
   * @private
   */
  enqueue (params) {
    this.bufferedBytes += params[1].length;
    this.queue.push(params);
  }

  /**
   * Sends a frame.
   *
   * @param {Buffer[]} list The frame to send
   * @param {Function} cb Callback
   * @private
   */
  sendFrame (list, cb) {
    if (list.length === 2) {
      this._socket.write(list[0]);
      this._socket.write(list[1], cb);
    } else {
      this._socket.write(list[0], cb);
    }
  }
}

module.exports = Sender;

/**
 * Converts an `ArrayBuffer` view into a buffer.
 *
 * @param {(DataView|TypedArray)} view The view to convert
 * @return {Buffer} Converted view
 * @private
 */
function viewToBuffer (view) {
  const buf = Buffer.from(view.buffer);

  if (view.byteLength !== view.buffer.byteLength) {
    return buf.slice(view.byteOffset, view.byteOffset + view.byteLength);
  }

  return buf;
}
<|endoftext|>


<commit_before>/*jslint white: true, onevar: true, undef: true, newcap: true, nomen: true, regexp: true, plusplus: true, bitwise: true, browser: true */
/*global $: false, google: false */

var Task, Turnpoint, Waypoint;

(function () {

	var computeDistanceBetween, computeHeading, computeLength, computeOffset;

	computeDistanceBetween = google.maps.geometry.spherical.computeDistanceBetween;
	computeHeading = google.maps.geometry.spherical.computeHeading;
	computeLength = google.maps.geometry.spherical.computeLength;
	computeOffset = google.maps.geometry.spherical.computeOffset;

	/* http://www.movable-type.co.uk/scripts/latlong.html */
	function computeCrossTrackDistance(from, to, point, radius) {
		var d13, theta12, theta13;
		radius = radius || 6378137;
		d13 = computeDistanceBetween(from, point, radius);
		theta12 = computeHeading(from, to);
		theta13 = computeHeading(from, point);
		return radius * Math.asin(Math.sin(d13 / radius) * Math.sin(Math.PI * (theta13 - theta12) / 180));
	}

	function computeAlongTrackDistance(from, to, point, radius) {
		var d13, dxt;
		radius = radius || 6378137;
		d13 = computeDistanceBetween(from, point, radius);
		dxt = computeCrossTrackDistance(from, to, point, radius);
		return radius * Math.acos(Math.cos(d13 / radius) / Math.cos(dxt / radius));
	}

	Turnpoint = function () {
		this.attributes = {};
		this.description = null;
		this.errors = [];
		this.id = null;
		this.position = null;
		this.radius = 400;
	};

	$.extend(Turnpoint, {
		ATTRIBUTES: {ss: true, es: true, gl: true}
	});

	$.extend(Turnpoint.prototype, {

		parse: function (s, waypoints) {
			var that = this;
			$.each(s.toLowerCase().split('.'), function (i, token) {
				var j;
				if (i === 0) {
					for (j = 0; j < waypoints.length; j += 1) {
						if (waypoints[j].id.substr(0, token.length).toLowerCase() === token) {
							that.id = waypoints[j].id;
							that.description = waypoints[j].description;
							that.position = waypoints[j].position;
							break;
						}
					}
					if (that.id === null) {
						that.errors.push('Invalid waypoint "' + token + '"');
					}
				} else {
					if (Turnpoint.ATTRIBUTES.hasOwnProperty(token)) {
						that.attributes[token] = true;
					} else if (token.match(/^r(\d+)(k?)$/)) {
						that.radius = (RegExp.$2 ? 1000 : 1) * parseInt(RegExp.$1, 10);
					} else {
						that.errors.push('Invalid token "' + token + '"');
					}
				}
			});
			return this;
		}

	});

	Task = function () {
		this.name = null;
		this.type = null;
		this.windowOpen = null;
		this.windowClose = null;
		this.startOpen = null;
		this.startLast = null;
		this.startClose = null;
		this.goalClose = null;
		this.taskClose = null;
		this.clockStart = null;
		this.turnpoints = [];
		this.errors = [];
	};

	$.extend(Task, {
		REFERENCE_TIME: {wc: 'windowOpen', so: 'windowOpen', sl: 'windowOpen', sc: 'windowOpen', gc: 'windowOpen', tc: 'windowOpen'},
		TIME_NAME: {wo: 'windowOpen', wc: 'windowClose', so: 'startOpen', sl: 'startLast', sc: 'startClose', gc: 'goalClose', tc: 'taskClose'},
		TYPES: {race: 'Race To Goal', open: 'Open Distance', elap: 'Elapsed Time', head: 'Headed Open Distance'}
	});

	$.extend(Task.prototype, {

		parse: function (s, waypoints, radius) {
			var i, that = this;
			$.each(s.split(/\s+/), function (i, token) {
				var referenceTime, time, turnpoint;
				if (i === 0) {
					token = token.toLowerCase();
					if (token !== 'tsk') {
						that.errors.push('Invalid task header "' + token + '"');
					}
				} else if (i === 1) {
					that.name = token;
				} else if (i === 2) {
					token = token.toLowerCase();
					if (Task.TYPES.hasOwnProperty(token)) {
						that.type = token;
					} else {
						that.errors.push('Invalid task type "' + token + '"');
					}
				} else {
					token = token.toLowerCase();
					/* FIXME handle cs */
					if (token.match(/^(wo)(\d\d)(\d\d)$/)) {
						that.windowOpen = 60 * parseInt(RegExp.$2, 10) + parseInt(RegExp.$3, 10);
					} else if (token.match(/^(wc|so|sl|sc|gc|tc)(\+)?(\d?\d)?(\d\d)$/)) {
						time = Task.TIME_NAME[RegExp.$1];
						that[time] = (RegExp.$3 ? 60 * parseInt(RegExp.$3, 10) : 0) + parseInt(RegExp.$4, 10);
						if (RegExp.$2) {
							referenceTime = that[Task.REFERENCE_TIME[RegExp.$1]];
							if (referenceTime !== null) {
								that[time] += referenceTime;
							} else {
								that.errors.push('Cannot specify relative time "' + time + '" when "' + Task.REFERENCE_TIME[RegExp.$1] + '" is not set');
							}
						}
					} else {
						turnpoint = new Turnpoint().parse(token, waypoints);
						$.each(turnpoint.errors, function (j, error) {
							that.errors.push(error);
						});
						that.turnpoints.push(turnpoint);
					}
				}
			});
			for (i = 0; i < this.turnpoints.length - 1; i += 1) {
				if (this.turnpoints[i].attributes.hasOwnProperty('ss') && this.turnpoints[i].name !== this.turnpoints[i + 1].name) {
					this.turnpoints[i].attributes.exit = true;
					break;
				}
			}
			this.computePath(radius);
			this.computeShortestPath(radius);
			return this;
		},

		computePath: function (radius) {
			this.path = $.map(this.turnpoints, function (turnpoint, i) {
				return turnpoint.position;
			});
			this.pathLength = computeLength(this.path, radius);
		},

		computeShortestPath: function (radius) {
			var alongTrackDistance, crossTrackDistance, heading, heading1, heading2, i, j, length, path, points;
			points = $.map(this.turnpoints, function (turnpoint, i) {
				return {
					center: turnpoint.position,
					include: true,
					radius: i === 0 || turnpoint.attributes.hasOwnProperty('gl') ? 0 : turnpoint.radius
				};
			});
			$.each(points, function (i, point) {
				var j, previous;
				if (i > 0) {
					for (j = i - 1; j >= 0; j -= 1) {
						if (points[j].include) {
							previous = points[j];
							break;
						}
					}
					if (computeDistanceBetween(previous.center, point.center, radius) < previous.radius) {
						previous.include = false;
					}
				}
			});
			points = $.map(points, function (point, i) {
				return point.include ? point : null;
			});
			path = $.map(points, function (point, i) {
				return point.center;
			});
			this.shortestPath = null;
			this.shortestPathLength = null;
			for (i = 0; i < 8; i += 1) {
				for (j = 1; j < points.length - 1; j += 1) {
					if (points[j].radius > 0) {
						crossTrackDistance = computeCrossTrackDistance(path[j - 1], path[j + 1], points[j].center, radius);
						if (Math.abs(crossTrackDistance) < points[j].radius) {
							alongTrackDistance = computeAlongTrackDistance(path[j - 1], path[j + 1], points[j].center, radius);
							heading = computeHeading(path[j - 1], path[j + 1]);
							path[j] = computeOffset(path[j - 1], alongTrackDistance, heading, radius);
							if (computeDistanceBetween(path[j], points[j].center, radius) > points[j].radius) {
								heading1 = computeHeading(points[j].center, path[j - 1]);
								heading2 = computeHeading(points[j].center, path[j + 1]);
								heading = (heading1 + heading2) / 2 + (Math.abs(heading1 - heading2) > 180 ? 180 : 0);
								path[j] = computeOffset(points[j].center, points[j].radius, heading, radius);
							}
						} else {
							heading1 = computeHeading(points[j].center, path[j - 1]);
							heading2 = computeHeading(points[j].center, path[j + 1]);
							heading = (heading1 + heading2) / 2 + (Math.abs(heading1 - heading2) > 180 ? 180 : 0);
							path[j] = computeOffset(points[j].center, points[j].radius, heading, radius);
						}
					}
				}
				if (points[points.length - 1].radius > 0) {
					heading = computeHeading(path[points.length - 1], path[points.length - 2]);
					path[points.length - 1] = computeOffset(points[points.length - 1].center, points[points.length - 1].radius, heading, radius);
				}
				length = computeLength(path, radius);
				if (i === 0 || length < this.shortestPathLength) {
					this.shortestPath = path;
					this.shortestPathLength = length;
				}
			}
		},

		getBounds: function () {
			var bounds;
			bounds = new google.maps.LatLngBounds();
			$.each(this.turnpoints, function (i, turnpoint) {
				bounds.extend(turnpoint.position);
			});
			return bounds;
		}

	});

	Waypoint = function (feature) {
		this.color = feature.properties.hasOwnProperty('color') ? '#' + feature.properties.color : null;
		this.description = feature.properties.hasOwnProperty('description') ? feature.properties.description : null;
		this.elevation = feature.geometry.coordinates.length > 2 ? feature.geometry.coordinates[2] : null;
		this.id = feature.properties.id;
		this.position = new google.maps.LatLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]);
		this.radius = feature.properties.hasOwnProperty('radius') ? feature.properties.radius : null;
	};

})();
<commit_msg>Add exit as a turnpoint attribute<commit_after>/*jslint white: true, onevar: true, undef: true, newcap: true, nomen: true, regexp: true, plusplus: true, bitwise: true, browser: true */
/*global $: false, google: false */

var Task, Turnpoint, Waypoint;

(function () {

	var computeDistanceBetween, computeHeading, computeLength, computeOffset;

	computeDistanceBetween = google.maps.geometry.spherical.computeDistanceBetween;
	computeHeading = google.maps.geometry.spherical.computeHeading;
	computeLength = google.maps.geometry.spherical.computeLength;
	computeOffset = google.maps.geometry.spherical.computeOffset;

	/* http://www.movable-type.co.uk/scripts/latlong.html */
	function computeCrossTrackDistance(from, to, point, radius) {
		var d13, theta12, theta13;
		radius = radius || 6378137;
		d13 = computeDistanceBetween(from, point, radius);
		theta12 = computeHeading(from, to);
		theta13 = computeHeading(from, point);
		return radius * Math.asin(Math.sin(d13 / radius) * Math.sin(Math.PI * (theta13 - theta12) / 180));
	}

	function computeAlongTrackDistance(from, to, point, radius) {
		var d13, dxt;
		radius = radius || 6378137;
		d13 = computeDistanceBetween(from, point, radius);
		dxt = computeCrossTrackDistance(from, to, point, radius);
		return radius * Math.acos(Math.cos(d13 / radius) / Math.cos(dxt / radius));
	}

	Turnpoint = function () {
		this.attributes = {};
		this.description = null;
		this.errors = [];
		this.id = null;
		this.position = null;
		this.radius = 400;
	};

	$.extend(Turnpoint, {
		ATTRIBUTES: {ss: true, es: true, exit: true, gl: true}
	});

	$.extend(Turnpoint.prototype, {

		parse: function (s, waypoints) {
			var that = this;
			$.each(s.toLowerCase().split('.'), function (i, token) {
				var j;
				if (i === 0) {
					for (j = 0; j < waypoints.length; j += 1) {
						if (waypoints[j].id.substr(0, token.length).toLowerCase() === token) {
							that.id = waypoints[j].id;
							that.description = waypoints[j].description;
							that.position = waypoints[j].position;
							break;
						}
					}
					if (that.id === null) {
						that.errors.push('Invalid waypoint "' + token + '"');
					}
				} else {
					if (Turnpoint.ATTRIBUTES.hasOwnProperty(token)) {
						that.attributes[token] = true;
					} else if (token.match(/^r(\d+)(k?)$/)) {
						that.radius = (RegExp.$2 ? 1000 : 1) * parseInt(RegExp.$1, 10);
					} else {
						that.errors.push('Invalid token "' + token + '"');
					}
				}
			});
			return this;
		}

	});

	Task = function () {
		this.name = null;
		this.type = null;
		this.windowOpen = null;
		this.windowClose = null;
		this.startOpen = null;
		this.startLast = null;
		this.startClose = null;
		this.goalClose = null;
		this.taskClose = null;
		this.clockStart = null;
		this.turnpoints = [];
		this.errors = [];
	};

	$.extend(Task, {
		REFERENCE_TIME: {wc: 'windowOpen', so: 'windowOpen', sl: 'windowOpen', sc: 'windowOpen', gc: 'windowOpen', tc: 'windowOpen'},
		TIME_NAME: {wo: 'windowOpen', wc: 'windowClose', so: 'startOpen', sl: 'startLast', sc: 'startClose', gc: 'goalClose', tc: 'taskClose'},
		TYPES: {race: 'Race To Goal', open: 'Open Distance', elap: 'Elapsed Time', head: 'Headed Open Distance'}
	});

	$.extend(Task.prototype, {

		parse: function (s, waypoints, radius) {
			var i, that = this;
			$.each(s.split(/\s+/), function (i, token) {
				var referenceTime, time, turnpoint;
				if (i === 0) {
					token = token.toLowerCase();
					if (token !== 'tsk') {
						that.errors.push('Invalid task header "' + token + '"');
					}
				} else if (i === 1) {
					that.name = token;
				} else if (i === 2) {
					token = token.toLowerCase();
					if (Task.TYPES.hasOwnProperty(token)) {
						that.type = token;
					} else {
						that.errors.push('Invalid task type "' + token + '"');
					}
				} else {
					token = token.toLowerCase();
					/* FIXME handle cs */
					if (token.match(/^(wo)(\d\d)(\d\d)$/)) {
						that.windowOpen = 60 * parseInt(RegExp.$2, 10) + parseInt(RegExp.$3, 10);
					} else if (token.match(/^(wc|so|sl|sc|gc|tc)(\+)?(\d?\d)?(\d\d)$/)) {
						time = Task.TIME_NAME[RegExp.$1];
						that[time] = (RegExp.$3 ? 60 * parseInt(RegExp.$3, 10) : 0) + parseInt(RegExp.$4, 10);
						if (RegExp.$2) {
							referenceTime = that[Task.REFERENCE_TIME[RegExp.$1]];
							if (referenceTime !== null) {
								that[time] += referenceTime;
							} else {
								that.errors.push('Cannot specify relative time "' + time + '" when "' + Task.REFERENCE_TIME[RegExp.$1] + '" is not set');
							}
						}
					} else {
						turnpoint = new Turnpoint().parse(token, waypoints);
						$.each(turnpoint.errors, function (j, error) {
							that.errors.push(error);
						});
						that.turnpoints.push(turnpoint);
					}
				}
			});
			for (i = 0; i < this.turnpoints.length - 1; i += 1) {
				if (this.turnpoints[i].attributes.hasOwnProperty('ss') && this.turnpoints[i].name !== this.turnpoints[i + 1].name) {
					this.turnpoints[i].attributes.exit = true;
					break;
				}
			}
			this.computePath(radius);
			this.computeShortestPath(radius);
			return this;
		},

		computePath: function (radius) {
			this.path = $.map(this.turnpoints, function (turnpoint, i) {
				return turnpoint.position;
			});
			this.pathLength = computeLength(this.path, radius);
		},

		computeShortestPath: function (radius) {
			var alongTrackDistance, crossTrackDistance, heading, heading1, heading2, i, j, length, path, points;
			points = $.map(this.turnpoints, function (turnpoint, i) {
				return {
					center: turnpoint.position,
					include: true,
					radius: i === 0 || turnpoint.attributes.hasOwnProperty('gl') ? 0 : turnpoint.radius
				};
			});
			$.each(points, function (i, point) {
				var j, previous;
				if (i > 0) {
					for (j = i - 1; j >= 0; j -= 1) {
						if (points[j].include) {
							previous = points[j];
							break;
						}
					}
					if (computeDistanceBetween(previous.center, point.center, radius) < previous.radius) {
						previous.include = false;
					}
				}
			});
			points = $.map(points, function (point, i) {
				return point.include ? point : null;
			});
			path = $.map(points, function (point, i) {
				return point.center;
			});
			this.shortestPath = null;
			this.shortestPathLength = null;
			for (i = 0; i < 8; i += 1) {
				for (j = 1; j < points.length - 1; j += 1) {
					if (points[j].radius > 0) {
						crossTrackDistance = computeCrossTrackDistance(path[j - 1], path[j + 1], points[j].center, radius);
						if (Math.abs(crossTrackDistance) < points[j].radius) {
							alongTrackDistance = computeAlongTrackDistance(path[j - 1], path[j + 1], points[j].center, radius);
							heading = computeHeading(path[j - 1], path[j + 1]);
							path[j] = computeOffset(path[j - 1], alongTrackDistance, heading, radius);
							if (computeDistanceBetween(path[j], points[j].center, radius) > points[j].radius) {
								heading1 = computeHeading(points[j].center, path[j - 1]);
								heading2 = computeHeading(points[j].center, path[j + 1]);
								heading = (heading1 + heading2) / 2 + (Math.abs(heading1 - heading2) > 180 ? 180 : 0);
								path[j] = computeOffset(points[j].center, points[j].radius, heading, radius);
							}
						} else {
							heading1 = computeHeading(points[j].center, path[j - 1]);
							heading2 = computeHeading(points[j].center, path[j + 1]);
							heading = (heading1 + heading2) / 2 + (Math.abs(heading1 - heading2) > 180 ? 180 : 0);
							path[j] = computeOffset(points[j].center, points[j].radius, heading, radius);
						}
					}
				}
				if (points[points.length - 1].radius > 0) {
					heading = computeHeading(path[points.length - 1], path[points.length - 2]);
					path[points.length - 1] = computeOffset(points[points.length - 1].center, points[points.length - 1].radius, heading, radius);
				}
				length = computeLength(path, radius);
				if (i === 0 || length < this.shortestPathLength) {
					this.shortestPath = path;
					this.shortestPathLength = length;
				}
			}
		},

		getBounds: function () {
			var bounds;
			bounds = new google.maps.LatLngBounds();
			$.each(this.turnpoints, function (i, turnpoint) {
				bounds.extend(turnpoint.position);
			});
			return bounds;
		}

	});

	Waypoint = function (feature) {
		this.color = feature.properties.hasOwnProperty('color') ? '#' + feature.properties.color : null;
		this.description = feature.properties.hasOwnProperty('description') ? feature.properties.description : null;
		this.elevation = feature.geometry.coordinates.length > 2 ? feature.geometry.coordinates[2] : null;
		this.id = feature.properties.id;
		this.position = new google.maps.LatLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]);
		this.radius = feature.properties.hasOwnProperty('radius') ? feature.properties.radius : null;
	};

})();
<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var path = require('path');
var util = require('util');
var _ = require('lodash');

// generateSecret is used to generate a one-off session secret if one wasn't configured
var generateSecret = require('./generateSecret');

// (this dependency is just for creating new cookies)
var uid = require('uid-safe').sync;

// (these two dependencies are only here for sails.session.parseSessionIdFromCookie(),
//  which is only here to enable socket lifecycle callbacks)
var parseCookie = require('cookie').parse;
var stringifyCookie = require('cookie').serialize;
var unsignCookie = require('cookie-signature').unsign;
var signCookie = require('cookie-signature').sign;

module.exports = function(app) {

  var getSession = function(sessionId, errorMessage, cb) {
    app.config.session.store.get(sessionId, function (err, session) {
      if (err) return cb(err);
      if (!session) {
        return cb((function _createError(){
          var e = new Error(errorMessage);
          e.code = 'E_SESSION';
          return e;
        })());
      }
      return cb(null, session);
    });
  };
  // `session` hook definition
  var SessionHook = {

    defaults: {
      session: {
        adapter: 'memory',
        key: 'sails.sid',
        resave: true,
        saveUninitialized: true
      }
    },

    /**
     * Normalize and validate configuration for this hook.
     * Then fold any modifications back into `sails.config`
     */
    configure: function() {

      // Validate config
      // Ensure that secret is specified if a custom session store is used
      if (app.config.session) {
        if (!_.isObject(app.config.session)) {
          throw new Error('Invalid custom session store configuration!\n' +
            '\n' +
            'Basic usage ::\n' +
            '{ session: { adapter: "memory", secret: "someVerySecureString", /* ...if applicable: host, port, etc... */ } }' +
            '\n\nCustom usage ::\n' +
            '{ session: { store: { /* some custom connect session store instance */ }, secret: "someVerySecureString", /* ...custom settings.... */ } }'
          );
        }

      }

      // If session config is set, but secret is undefined, set a secure, one-time use secret
      if (!app.config.session || !app.config.session.secret) {

        app.log.verbose('Session secret not defined-- automatically generating one for now...');

        if (app.config.environment === 'production') {
          app.log.warn('Session secret should be manually specified in production!');
          app.log.warn('Automatically generating one for now...');

          app.log.error('This generated session secret is NOT OK for production!');
          app.log.error('It will change each time the server starts and break multi-instance deployments.');
          app.log.blank();
          app.log.error('To set up a session secret, add or update it in `config/session.js`:');
          app.log.error('module.exports.session = { secret: "keyboardcat" }');
          app.log.blank();
        }

        app.config.session.secret = generateSecret();
      }

      // Backwards-compatibility / shorthand notation
      // (allow mongo or redis session stores to be specified directly)
      if (app.config.session.adapter === 'redis') {
        app.config.session.adapter = 'connect-redis';
      }
      else if (app.config.session.adapter === 'mongo') {
        app.config.session.adapter = 'connect-mongo';
      }
    },

    /**
     * Create a connection to the configured session store
     * and keep it around
     *
     * @api private
     */
    initialize: function(cb) {
      var sessionConfig = app.config.session;
      // console.log('Initializing session hook...');

      // Intepret session adapter config and "new up" a session store
      if (_.isObject(sessionConfig) && !_.isObject(sessionConfig.store)) {

        // Unless the session is explicitly disabled, require the appropriate adapter
        if (sessionConfig.adapter) {

          // 'memory' is a special case
          if (sessionConfig.adapter === 'memory') {
            var session = require('express-session');
            var MemoryStore = session.MemoryStore;
            sessionConfig.store = new MemoryStore();
          }
          // Try and load the specified adapter from the local sails project,
          // or catch and return error:
          else {

            var COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR = function (adapter, packagejson, e) {
              var errMsg;
              if (e && typeof e === 'object' && e instanceof Error) {
                errMsg = e.stack;
              }
              else {
                errMsg = util.inspect(e);
              }

              var output = 'Could not load express-session adapter :: ' + adapter + '\n';
              if (packagejson && !packagejson.main) {
                output+='(If this is your module, make sure that the module has a "main" configuration in its package.json file)';
              }
              output+='\nError from adapter:\n'+ errMsg+'\n\n';

              // Recommend installation of the session adapter:
              output += 'Do you have the express-session adapter installed in this project?\n';
              output += 'Try running the following command in your project\'s root directory:\n';
              var installRecommendation = 'npm install ';
              if (adapter === 'connect-redis') {
                installRecommendation += 'connect-redis';
              }
              else {
                installRecommendation += adapter;
                installRecommendation +='\n(Note: Make sure the version of the Connect adapter you install is compatible with Express 4/Sails v0.10)';
              }
              installRecommendation += '\n';

              output += installRecommendation;

              return output;
            };

            try {

              // Determine the path to the adapter by using the "main" described in its package.json file:
              var pathToAdapterDependency;
              var pathToAdapterPackage = path.resolve(app.config.appPath, 'node_modules', sessionConfig.adapter ,'package.json');
              var adapterPackage;
              try {
                adapterPackage = require(pathToAdapterPackage);
                pathToAdapterDependency = path.resolve(app.config.appPath, 'node_modules', sessionConfig.adapter, adapterPackage.main);
              }
              catch (e) {
                return cb(COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR(sessionConfig.adapter, adapterPackage, e));
              }
              var SessionAdapter = require(pathToAdapterDependency);
              var CustomStore = SessionAdapter(require('express'));
              sessionConfig.store = new CustomStore(sessionConfig);
            } catch (e) {
              // TODO: negotiate error and give better error msg depending on code
              return cb(COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR(sessionConfig.adapter, adapterPackage, e));
            }
          }
        }
      }

      // Expose hook as `sails.session`
      app.session = SessionHook;

      return cb();
    },

    /**
     * Generate a cookie to represent a new session.
     *
     * @return {String}
     * @api private
     */

    generateNewSidCookie: function (){
      var sid = uid(24);
      var signedSid = 's:' + signCookie(sid, app.config.session.secret);
      var cookie = stringifyCookie(app.config.session.key, signedSid, {});
      return cookie;
    },

    /**
     * Parse and unsign (i.e. decrypt) the provided cookie to get the session id.
     *
     * (adapted from code in the `express-session`)
     * (TODO: pull out into separate module as part of jshttp/pillarjs)
     *
     * @param  {String} cookie
     * @return {String}                [sessionId]
     *
     * @throws {Error} If cookie cannot be parsed or unsigned
     */
    parseSessionIdFromCookie: function (cookie){

      // e.g. "lolcatparty"
      var sessionSecret = app.config.session.secret;

      // Parse cookie
      var parsedSidCookie = parseCookie(cookie)[app.config.session.key];

      if (typeof parsedSidCookie !== 'string') {
        throw (function createError(){
          var err = new Error('No sid cookie exists');
          err.status = 401;
          err.code = 'E_SESSION_PARSE_COOKIE';
          return err;
        })();
      }

      if (parsedSidCookie.substr(0, 2) !== 's:') {
        throw (function createError(){
          var err = new Error('Cookie unsigned');
          err.status = 401;
          err.code = 'E_SESSION_PARSE_COOKIE';
          return err;
        })();
      }

      // Unsign cookie
      var sessionId = unsignCookie(parsedSidCookie.slice(2), sessionSecret);

      if (sessionId === false) {
        throw (function createError(){
          var err = new Error('Cookie signature invalid');
          err.status = 401;
          err.code = 'E_SESSION_PARSE_COOKIE';
          return err;
        })();
      }

      return sessionId;
    },

    /**
     * @param {String} sessionId
     * @param {Function} cb
     *
     * @api private
     */
    get: function(sessionId, cb) {
      if (!_.isFunction(cb)) {
        throw new Error('Invalid usage :: `sails.hooks.session.get(sessionId, cb)`');
      }
      return getSession(sessionId, 'Session could not be loaded', cb);
    },

    /**
     * @param {String} sessionId
     * @param {} data
     * @param {Function} cb
     *
     * @api private
     */
    set: function(sessionId, data, cb) {
      if (!_.isFunction(cb)) {
        throw new Error('Invalid usage :: `sails.hooks.session.set(sessionId, data, cb)`');
      }
      return app.config.session.store.set(sessionId, data, function (err) {
        if (err) return cb(err);
        return getSession(sessionId, 'Session could not be saved', cb);
      });
    }
  };

  return SessionHook;
};
<commit_msg>Sessions working with last versions of connect-redis and connect-mongo<commit_after>/**
 * Module dependencies.
 */

var path = require('path');
var util = require('util');
var _ = require('lodash');

// generateSecret is used to generate a one-off session secret if one wasn't configured
var generateSecret = require('./generateSecret');

// (this dependency is just for creating new cookies)
var uid = require('uid-safe').sync;

// (these two dependencies are only here for sails.session.parseSessionIdFromCookie(),
//  which is only here to enable socket lifecycle callbacks)
var parseCookie = require('cookie').parse;
var stringifyCookie = require('cookie').serialize;
var unsignCookie = require('cookie-signature').unsign;
var signCookie = require('cookie-signature').sign;

module.exports = function(app) {

  var getSession = function(sessionId, errorMessage, cb) {
    app.config.session.store.get(sessionId, function (err, session) {
      if (err) return cb(err);
      if (!session) {
        return cb((function _createError(){
          var e = new Error(errorMessage);
          e.code = 'E_SESSION';
          return e;
        })());
      }
      return cb(null, session);
    });
  };
  // `session` hook definition
  var SessionHook = {

    defaults: {
      session: {
        adapter: 'memory',
        key: 'sails.sid',
        resave: true,
        saveUninitialized: true
      }
    },

    /**
     * Normalize and validate configuration for this hook.
     * Then fold any modifications back into `sails.config`
     */
    configure: function() {

      // Validate config
      // Ensure that secret is specified if a custom session store is used
      if (app.config.session) {
        if (!_.isObject(app.config.session)) {
          throw new Error('Invalid custom session store configuration!\n' +
            '\n' +
            'Basic usage ::\n' +
            '{ session: { adapter: "memory", secret: "someVerySecureString", /* ...if applicable: host, port, etc... */ } }' +
            '\n\nCustom usage ::\n' +
            '{ session: { store: { /* some custom connect session store instance */ }, secret: "someVerySecureString", /* ...custom settings.... */ } }'
          );
        }

      }

      // If session config is set, but secret is undefined, set a secure, one-time use secret
      if (!app.config.session || !app.config.session.secret) {

        app.log.verbose('Session secret not defined-- automatically generating one for now...');

        if (app.config.environment === 'production') {
          app.log.warn('Session secret should be manually specified in production!');
          app.log.warn('Automatically generating one for now...');

          app.log.error('This generated session secret is NOT OK for production!');
          app.log.error('It will change each time the server starts and break multi-instance deployments.');
          app.log.blank();
          app.log.error('To set up a session secret, add or update it in `config/session.js`:');
          app.log.error('module.exports.session = { secret: "keyboardcat" }');
          app.log.blank();
        }

        app.config.session.secret = generateSecret();
      }

      // Backwards-compatibility / shorthand notation
      // (allow mongo or redis session stores to be specified directly)
      if (app.config.session.adapter === 'redis') {
        app.config.session.adapter = 'connect-redis';
      }
      else if (app.config.session.adapter === 'mongo') {
        app.config.session.adapter = 'connect-mongo';
      }
    },

    /**
     * Create a connection to the configured session store
     * and keep it around
     *
     * @api private
     */
    initialize: function(cb) {
      var sessionConfig = app.config.session;
      // console.log('Initializing session hook...');

      // Intepret session adapter config and "new up" a session store
      if (_.isObject(sessionConfig) && !_.isObject(sessionConfig.store)) {

        // Unless the session is explicitly disabled, require the appropriate adapter
        if (sessionConfig.adapter) {

          // 'memory' is a special case
          if (sessionConfig.adapter === 'memory') {
            var session = require('express-session');
            var MemoryStore = session.MemoryStore;
            sessionConfig.store = new MemoryStore();
          }
          // Try and load the specified adapter from the local sails project,
          // or catch and return error:
          else {

            var COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR = function (adapter, packagejson, e) {
              var errMsg;
              if (e && typeof e === 'object' && e instanceof Error) {
                errMsg = e.stack;
              }
              else {
                errMsg = util.inspect(e);
              }

              var output = 'Could not load express-session adapter :: ' + adapter + '\n';
              if (packagejson && !packagejson.main) {
                output+='(If this is your module, make sure that the module has a "main" configuration in its package.json file)';
              }
              output+='\nError from adapter:\n'+ errMsg+'\n\n';

              // Recommend installation of the session adapter:
              output += 'Do you have the express-session adapter installed in this project?\n';
              output += 'Try running the following command in your project\'s root directory:\n';
              var installRecommendation = 'npm install ';
              if (adapter === 'connect-redis') {
                installRecommendation += 'connect-redis';
              }
              else {
                installRecommendation += adapter;
                installRecommendation +='\n(Note: Make sure the version of the Connect adapter you install is compatible with Express 4/Sails v0.12)';
              }
              installRecommendation += '\n';

              output += installRecommendation;

              return output;
            };

            try {

              // Determine the path to the adapter by using the "main" described in its package.json file:
              var pathToAdapterDependency;
              var pathToAdapterPackage = path.resolve(app.config.appPath, 'node_modules', sessionConfig.adapter ,'package.json');
              var adapterPackage;
              try {
                adapterPackage = require(pathToAdapterPackage);
                pathToAdapterDependency = path.resolve(app.config.appPath, 'node_modules', sessionConfig.adapter, adapterPackage.main);
              }
              catch (e) {
                return cb(COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR(sessionConfig.adapter, adapterPackage, e));
              }

              var session = require('express-session');
              var CustomStore = require(pathToAdapterDependency)(session);
              sessionConfig.store = new CustomStore(sessionConfig);
              sessionConfig.secret = app.config.session.secret;

            } catch (e) {
              // TODO: negotiate error and give better error msg depending on code
              return cb(COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR(sessionConfig.adapter, adapterPackage, e));
            }
          }
        }
      }

      // Expose hook as `sails.session`
      app.session = SessionHook;

      return cb();
    },

    /**
     * Generate a cookie to represent a new session.
     *
     * @return {String}
     * @api private
     */

    generateNewSidCookie: function (){
      var sid = uid(24);
      var signedSid = 's:' + signCookie(sid, app.config.session.secret);
      var cookie = stringifyCookie(app.config.session.key, signedSid, {});
      return cookie;
    },

    /**
     * Parse and unsign (i.e. decrypt) the provided cookie to get the session id.
     *
     * (adapted from code in the `express-session`)
     * (TODO: pull out into separate module as part of jshttp/pillarjs)
     *
     * @param  {String} cookie
     * @return {String}                [sessionId]
     *
     * @throws {Error} If cookie cannot be parsed or unsigned
     */
    parseSessionIdFromCookie: function (cookie){

      // e.g. "lolcatparty"
      var sessionSecret = app.config.session.secret;

      // Parse cookie
      var parsedSidCookie = parseCookie(cookie)[app.config.session.key];

      if (typeof parsedSidCookie !== 'string') {
        throw (function createError(){
          var err = new Error('No sid cookie exists');
          err.status = 401;
          err.code = 'E_SESSION_PARSE_COOKIE';
          return err;
        })();
      }

      if (parsedSidCookie.substr(0, 2) !== 's:') {
        throw (function createError(){
          var err = new Error('Cookie unsigned');
          err.status = 401;
          err.code = 'E_SESSION_PARSE_COOKIE';
          return err;
        })();
      }

      // Unsign cookie
      var sessionId = unsignCookie(parsedSidCookie.slice(2), sessionSecret);

      if (sessionId === false) {
        throw (function createError(){
          var err = new Error('Cookie signature invalid');
          err.status = 401;
          err.code = 'E_SESSION_PARSE_COOKIE';
          return err;
        })();
      }

      return sessionId;
    },

    /**
     * @param {String} sessionId
     * @param {Function} cb
     *
     * @api private
     */
    get: function(sessionId, cb) {
      if (!_.isFunction(cb)) {
        throw new Error('Invalid usage :: `sails.hooks.session.get(sessionId, cb)`');
      }
      return getSession(sessionId, 'Session could not be loaded', cb);
    },

    /**
     * @param {String} sessionId
     * @param {} data
     * @param {Function} cb
     *
     * @api private
     */
    set: function(sessionId, data, cb) {
      if (!_.isFunction(cb)) {
        throw new Error('Invalid usage :: `sails.hooks.session.set(sessionId, data, cb)`');
      }
      return app.config.session.store.set(sessionId, data, function (err) {
        if (err) return cb(err);
        return getSession(sessionId, 'Session could not be saved', cb);
      });
    }
  };

  return SessionHook;
};
<|endoftext|>