<commit_before>'use strict';

var https = require('https'),
    zlib = require('zlib'),
    url = require('url'),
    pkg = require('../package.json'),
    error = require('./error.js'),
    util = require('./util.js');

module.exports = DiscogsClient;

/**
 * Default configuration
 */

var defaultConfig = {
    host: 'api.discogs.com',
    port: 443,
    userAgent: 'DisConnectClient/' + pkg.version + ' +' + pkg.homepage,
    apiVersion: 'v2',
    outputFormat: 'discogs',    // Possible values: 'discogs' / 'plaintext' / 'html'
    requestLimit: 25,           // Maximum number of requests to the Discogs API per interval
    requestLimitAuth: 60,       // Maximum number of requests to the Discogs API per interval when authenticated
    requestLimitInterval: 60000 // Request interval in milliseconds
};

/**
 * The request queue, shared by all DiscogsClient instances
 * @type {Queue}
 */

var queue = require('./queue.js')({
    maxCalls: defaultConfig.requestLimit,
    interval: defaultConfig.requestLimitInterval
});

/**
 * Object constructor
 * @param {string} [userAgent] - The name of the user agent to use to make API calls
 * @param {object} [auth] - Optional authorization data object
 * @return {DiscogsClient}
 */

function DiscogsClient(userAgent, auth) {
    // Allow the class to be called as a function, returning an instance
    if (!(this instanceof DiscogsClient)) {
        return new DiscogsClient(userAgent, auth);
    }
    // Set the default configuration
    this.config = util.merge({}, defaultConfig);
    // Set the custom User Agent when provided
    if (typeof userAgent === 'string') {
        this.config.userAgent = userAgent;
    }
    // No userAgent provided, but instead we have an accessObject
    if ((arguments.length === 1) && (typeof userAgent === 'object')) {
        auth = userAgent;
    }
    // Set auth data when provided
    if (auth && (typeof auth === 'object')) {
        queue.setConfig({maxCalls: this.config.requestLimitAuth});
        if (!auth.hasOwnProperty('method')) {
            auth.method = 'discogs';
        }
        if (!auth.hasOwnProperty('level')) {
            if (auth.userToken) {
                auth.level = 2;
            } else if (auth.consumerKey && auth.consumerSecret) {
                auth.level = 1;
            }
        }
        this.auth = util.merge({}, auth);
    // Unauthenticated new client instances will decrease the shared request limit
    } else {
        queue.setConfig({maxCalls: this.config.requestLimit});
    }
}

/**
 * Override the default configuration
 * @param {object} customConfig - Custom configuration object for Browserify/CORS/Proxy use cases
 * @return {DiscogsClient}
 */
DiscogsClient.prototype.setConfig = function(customConfig) {
    util.merge(this.config, customConfig);
    queue.setConfig({
        maxCalls: (this.authenticated() ? this.config.requestLimitAuth : this.config.requestLimit),
        interval: this.config.requestLimitInterval
    });
    return this;
};

/**
 * Return whether the client is authenticated for the optionally given access level
 * @param {number} [level] - Optional authentication level
 * @return {boolean}
 */

DiscogsClient.prototype.authenticated = function(level) {
    level = level || 0;
    return (!(typeof this.auth === 'undefined') && (this.auth.level > 0) && (this.auth.level >= level));
};

/**
 * Test authentication by getting the identity resource for the authenticated user
 * @param {function} callback - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.getIdentity = function(callback) {
    return this.get({url: '/oauth/identity', authLevel: 2}, callback);
};

/**
 * Get info about the Discogs API and this client
 * @param {function} callback - Callback function receiving the data
 */

DiscogsClient.prototype.about = function(callback) {
    var clientInfo = {
        version: pkg.version,
        userAgent: this.config.userAgent,
        authMethod: (this.auth ? this.auth.method : 'none'),
        authLevel: (this.auth ? this.auth.level : 0)
    };
    if (typeof callback === 'function') {
        return this.get('', function(err, data) {
            data && (data.disconnect = clientInfo);
            callback(err, data);
        });
    }
    return this.get('').then(function(data) {
        data && (data.disconnect = clientInfo);
        return data;
    });
};

/**
 * Send a raw request
 * @param {object} options - Request options
 * {
 *		url: '', // May be a relative path when accessing the discogs API
 *		method: '', // Defaults to GET
 *		data: {} // POST/PUT data as an object
 * }
 * @param {function} callback - Callback function receiving the data
 * @return {DiscogsClient}
 */

DiscogsClient.prototype._rawRequest = function(options, callback) {
    var data = options.data || null,
        method = options.method || 'GET',
        urlParts = url.parse(options.url),
        encoding = options.encoding || 'utf8';

    // Build request headers
    var headers = {
        'User-Agent': this.config.userAgent,
        'Accept': 'application/json,application/vnd.discogs.' + this.config.apiVersion + '.' + this.config.outputFormat + '+json,application/octet-stream',
        'Accept-Encoding': 'gzip,deflate',
        'Host': urlParts.host || this.config.host,
        'Connection': 'close',
        'Content-Length': 0
    };

    // Add content headers for POST/PUT requests that contain data
    if (data) {
        if (typeof data === 'object') {
            data = JSON.stringify(data);
        } // Convert data to a JSON string when data is an object/array
        headers['Content-Type'] = 'application/json'; // Discogs accepts data in JSON format
        headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
    }

    // Add Authorization header when authenticated (or in the process of authenticating)
    if (this.auth && (this.auth.consumerKey || this.auth.userToken)) {
        var authHeader = '';
        if (this.auth.method === 'oauth') {
            var fullUrl = (urlParts.protocol && urlParts.host) ? urlParts.href : 'https://' + this.config.host + urlParts.path;
            authHeader = this.oauth().toHeader(method, fullUrl);
        } else if (this.auth.method === 'discogs') {
            authHeader = 'Discogs';
            if (this.auth.userToken) {
                authHeader += ' token=' + this.auth.userToken;
            } else if (this.auth.consumerKey) {
                authHeader += ' key=' + this.auth.consumerKey + ', secret=' + this.auth.consumerSecret;
            }
        }
        headers['Authorization'] = authHeader;
    }

    // Set the HTTPS request options
    var requestOptions = {
        host: urlParts.host || this.config.host,
        port: urlParts.port || this.config.port,
        path: urlParts.path,
        method: method,
        headers: headers
    };

    // Build the HTTPS request
    var req = https.request(requestOptions, function(res) {
        var data = '', rateLimit = null, add = function(chunk) {
            data += chunk.toString();
        };

        // Pass the data to the callback and pass an error on unsuccessful HTTP status
        var passData = function() {
            var err = null, status = parseInt(res.statusCode, 10);
            if (status > 399) { // Unsuccessful HTTP status? Then pass an error to the callback
                var match = data.match(/^\{"message": "(.+)"\}/i);
                err = new error.DiscogsError(status, ((match && match[1]) ? match[1] : null));
            }
            callback(err, data, rateLimit);
        };

        // Find and add rate limiting when present
        if (res.headers['x-discogs-ratelimit']) {
            rateLimit = {
                limit: parseInt(res.headers['x-discogs-ratelimit'], 10),
                used: parseInt(res.headers['x-discogs-ratelimit-used'], 10),
                remaining: parseInt(res.headers['x-discogs-ratelimit-remaining'], 10)
            };
        }

        // Get the response content and pass it to the callback
        switch (res.headers['content-encoding']) {
            case 'gzip':
                var gunzip = zlib.createGunzip().on('data', add).on('end', passData);
                res.pipe(gunzip);
                break;
            case 'deflate':
                var inflate = zlib.createInflate().on('data', add).on('end', passData);
                res.pipe(inflate);
                break;
            default:
                // Set encoding when provided
                res.setEncoding(encoding);
                res.on('data', add).on('end', passData);
        }
    }).on('error', function(err) {
        callback(err);
    });

    // When present, write the data to the request
    if (data) {
        req.write(data);
    }

    req.end();
    return this;
};

/**
 * Send a request and parse text response to JSON
 * @param {object} options - Request options
 * {
 *		url: '', // May be a relative path when accessing the Discogs API
 *		method: '', // Defaults to GET
 *		data: {} // POST/PUT data as an object
 * }
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype._request = function(options, callback) {
    var client = this,
            doRequest = function() {
                client._rawRequest(options, function(err, data, rateLimit) {
                    if (data && options.json && (data.indexOf('<!') !== 0)) {
                        data = JSON.parse(data);
                    }
                    callback(err, data, rateLimit);
                });
            },
            prepareRequest = function() {
                // Check whether authentication is required
                if (!options.authLevel || client.authenticated(options.authLevel)) {
                    if (options.queue) { // Add API request to the execution queue
                        queue.add(function(err) {
                            if (!err) {
                                doRequest(callback);
                            } else { // Can't add to the queue because it's full
                                callback(err);
                            }
                        });
                    } else { // Don't queue, just do the request
                        doRequest(callback);
                    }
                } else {
                    callback(new error.AuthError());
                }
            };

    // By default, queue requests
    if (!options.hasOwnProperty('queue')) {
        options.queue = true;
    }
    // By default, expect responses to be JSON
    if (!options.hasOwnProperty('json')) {
        options.json = true;
    }

    if (typeof callback === 'function') {
        prepareRequest();
        return this;
    }
    // No callback provided? Return a Promise
    return new Promise(function(resolve, reject) {
        callback = function(err, data) {
            (err && reject(err)) || resolve(data);
        };
        prepareRequest();
    });
};

/**
 * Perform a GET request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.get = function(options, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(options, callback);
};

/**
 * Perform a POST request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {object} data - POST data
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.post = function(options, data, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(util.merge(options, {method: 'POST', data: data}), callback);
};

/**
 * Perform a PUT request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {object} data - PUT data
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.put = function(options, data, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(util.merge(options, {method: 'PUT', data: data}), callback);
};

/**
 * Perform a DELETE request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.delete = function(options, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(util.merge(options, {method: 'DELETE'}), callback);
};

/**
 * Get an instance of the Discogs OAuth class
 * @return {DiscogsOAuth}
 */

DiscogsClient.prototype.oauth = function() {
    var OAuth = require('./oauth.js');
    return new OAuth(this.auth);
};

/**
 * Expose the database functions and pass the current instance
 * @return {object}
 */

DiscogsClient.prototype.database = function() {
    return require('./database.js')(this);
};

/**
 * Expose the marketplace functions and pass the current instance
 * @return {object}
 */

DiscogsClient.prototype.marketplace = function() {
    return require('./marketplace.js')(this);
};

/**
 * Expose the user functions and pass the current instance
 * @return {object}
 */

DiscogsClient.prototype.user = function() {
    return require('./user.js')(this);
};<commit_msg>Fix outputFormat (#67)<commit_after>'use strict';

var https = require('https'),
    zlib = require('zlib'),
    url = require('url'),
    pkg = require('../package.json'),
    error = require('./error.js'),
    util = require('./util.js');

module.exports = DiscogsClient;

/**
 * Default configuration
 */

var defaultConfig = {
    host: 'api.discogs.com',
    port: 443,
    userAgent: 'DisConnectClient/' + pkg.version + ' +' + pkg.homepage,
    apiVersion: 'v2',
    outputFormat: 'discogs',    // Possible values: 'discogs' / 'plaintext' / 'html'
    requestLimit: 25,           // Maximum number of requests to the Discogs API per interval
    requestLimitAuth: 60,       // Maximum number of requests to the Discogs API per interval when authenticated
    requestLimitInterval: 60000 // Request interval in milliseconds
};

/**
 * The request queue, shared by all DiscogsClient instances
 * @type {Queue}
 */

var queue = require('./queue.js')({
    maxCalls: defaultConfig.requestLimit,
    interval: defaultConfig.requestLimitInterval
});

/**
 * Object constructor
 * @param {string} [userAgent] - The name of the user agent to use to make API calls
 * @param {object} [auth] - Optional authorization data object
 * @return {DiscogsClient}
 */

function DiscogsClient(userAgent, auth) {
    // Allow the class to be called as a function, returning an instance
    if (!(this instanceof DiscogsClient)) {
        return new DiscogsClient(userAgent, auth);
    }
    // Set the default configuration
    this.config = util.merge({}, defaultConfig);
    // Set the custom User Agent when provided
    if (typeof userAgent === 'string') {
        this.config.userAgent = userAgent;
    }
    // No userAgent provided, but instead we have an accessObject
    if ((arguments.length === 1) && (typeof userAgent === 'object')) {
        auth = userAgent;
    }
    // Set auth data when provided
    if (auth && (typeof auth === 'object')) {
        queue.setConfig({maxCalls: this.config.requestLimitAuth});
        if (!auth.hasOwnProperty('method')) {
            auth.method = 'discogs';
        }
        if (!auth.hasOwnProperty('level')) {
            if (auth.userToken) {
                auth.level = 2;
            } else if (auth.consumerKey && auth.consumerSecret) {
                auth.level = 1;
            }
        }
        this.auth = util.merge({}, auth);
    // Unauthenticated new client instances will decrease the shared request limit
    } else {
        queue.setConfig({maxCalls: this.config.requestLimit});
    }
}

/**
 * Override the default configuration
 * @param {object} customConfig - Custom configuration object for Browserify/CORS/Proxy use cases
 * @return {DiscogsClient}
 */
DiscogsClient.prototype.setConfig = function(customConfig) {
    util.merge(this.config, customConfig);
    queue.setConfig({
        maxCalls: (this.authenticated() ? this.config.requestLimitAuth : this.config.requestLimit),
        interval: this.config.requestLimitInterval
    });
    return this;
};

/**
 * Return whether the client is authenticated for the optionally given access level
 * @param {number} [level] - Optional authentication level
 * @return {boolean}
 */

DiscogsClient.prototype.authenticated = function(level) {
    level = level || 0;
    return (!(typeof this.auth === 'undefined') && (this.auth.level > 0) && (this.auth.level >= level));
};

/**
 * Test authentication by getting the identity resource for the authenticated user
 * @param {function} callback - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.getIdentity = function(callback) {
    return this.get({url: '/oauth/identity', authLevel: 2}, callback);
};

/**
 * Get info about the Discogs API and this client
 * @param {function} callback - Callback function receiving the data
 */

DiscogsClient.prototype.about = function(callback) {
    var clientInfo = {
        version: pkg.version,
        userAgent: this.config.userAgent,
        authMethod: (this.auth ? this.auth.method : 'none'),
        authLevel: (this.auth ? this.auth.level : 0)
    };
    if (typeof callback === 'function') {
        return this.get('', function(err, data) {
            data && (data.disconnect = clientInfo);
            callback(err, data);
        });
    }
    return this.get('').then(function(data) {
        data && (data.disconnect = clientInfo);
        return data;
    });
};

/**
 * Send a raw request
 * @param {object} options - Request options
 * {
 *		url: '', // May be a relative path when accessing the discogs API
 *		method: '', // Defaults to GET
 *		data: {} // POST/PUT data as an object
 * }
 * @param {function} callback - Callback function receiving the data
 * @return {DiscogsClient}
 */

DiscogsClient.prototype._rawRequest = function(options, callback) {
    var data = options.data || null,
        method = options.method || 'GET',
        urlParts = url.parse(options.url),
        encoding = options.encoding || 'utf8';

    // Build request headers
    var headers = {
        'User-Agent': this.config.userAgent,
        'Accept': 'application/vnd.discogs.' + this.config.apiVersion + '.' + this.config.outputFormat + '+json,application/octet-stream',
        'Accept-Encoding': 'gzip,deflate',
        'Host': urlParts.host || this.config.host,
        'Connection': 'close',
        'Content-Length': 0
    };

    // Add content headers for POST/PUT requests that contain data
    if (data) {
        if (typeof data === 'object') {
            data = JSON.stringify(data);
        } // Convert data to a JSON string when data is an object/array
        headers['Content-Type'] = 'application/json'; // Discogs accepts data in JSON format
        headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
    }

    // Add Authorization header when authenticated (or in the process of authenticating)
    if (this.auth && (this.auth.consumerKey || this.auth.userToken)) {
        var authHeader = '';
        if (this.auth.method === 'oauth') {
            var fullUrl = (urlParts.protocol && urlParts.host) ? urlParts.href : 'https://' + this.config.host + urlParts.path;
            authHeader = this.oauth().toHeader(method, fullUrl);
        } else if (this.auth.method === 'discogs') {
            authHeader = 'Discogs';
            if (this.auth.userToken) {
                authHeader += ' token=' + this.auth.userToken;
            } else if (this.auth.consumerKey) {
                authHeader += ' key=' + this.auth.consumerKey + ', secret=' + this.auth.consumerSecret;
            }
        }
        headers['Authorization'] = authHeader;
    }

    // Set the HTTPS request options
    var requestOptions = {
        host: urlParts.host || this.config.host,
        port: urlParts.port || this.config.port,
        path: urlParts.path,
        method: method,
        headers: headers
    };

    // Build the HTTPS request
    var req = https.request(requestOptions, function(res) {
        var data = '', rateLimit = null, add = function(chunk) {
            data += chunk.toString();
        };

        // Pass the data to the callback and pass an error on unsuccessful HTTP status
        var passData = function() {
            var err = null, status = parseInt(res.statusCode, 10);
            if (status > 399) { // Unsuccessful HTTP status? Then pass an error to the callback
                var match = data.match(/^\{"message": "(.+)"\}/i);
                err = new error.DiscogsError(status, ((match && match[1]) ? match[1] : null));
            }
            callback(err, data, rateLimit);
        };

        // Find and add rate limiting when present
        if (res.headers['x-discogs-ratelimit']) {
            rateLimit = {
                limit: parseInt(res.headers['x-discogs-ratelimit'], 10),
                used: parseInt(res.headers['x-discogs-ratelimit-used'], 10),
                remaining: parseInt(res.headers['x-discogs-ratelimit-remaining'], 10)
            };
        }

        // Get the response content and pass it to the callback
        switch (res.headers['content-encoding']) {
            case 'gzip':
                var gunzip = zlib.createGunzip().on('data', add).on('end', passData);
                res.pipe(gunzip);
                break;
            case 'deflate':
                var inflate = zlib.createInflate().on('data', add).on('end', passData);
                res.pipe(inflate);
                break;
            default:
                // Set encoding when provided
                res.setEncoding(encoding);
                res.on('data', add).on('end', passData);
        }
    }).on('error', function(err) {
        callback(err);
    });

    // When present, write the data to the request
    if (data) {
        req.write(data);
    }

    req.end();
    return this;
};

/**
 * Send a request and parse text response to JSON
 * @param {object} options - Request options
 * {
 *		url: '', // May be a relative path when accessing the Discogs API
 *		method: '', // Defaults to GET
 *		data: {} // POST/PUT data as an object
 * }
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype._request = function(options, callback) {
    var client = this,
            doRequest = function() {
                client._rawRequest(options, function(err, data, rateLimit) {
                    if (data && options.json && (data.indexOf('<!') !== 0)) {
                        data = JSON.parse(data);
                    }
                    callback(err, data, rateLimit);
                });
            },
            prepareRequest = function() {
                // Check whether authentication is required
                if (!options.authLevel || client.authenticated(options.authLevel)) {
                    if (options.queue) { // Add API request to the execution queue
                        queue.add(function(err) {
                            if (!err) {
                                doRequest(callback);
                            } else { // Can't add to the queue because it's full
                                callback(err);
                            }
                        });
                    } else { // Don't queue, just do the request
                        doRequest(callback);
                    }
                } else {
                    callback(new error.AuthError());
                }
            };

    // By default, queue requests
    if (!options.hasOwnProperty('queue')) {
        options.queue = true;
    }
    // By default, expect responses to be JSON
    if (!options.hasOwnProperty('json')) {
        options.json = true;
    }

    if (typeof callback === 'function') {
        prepareRequest();
        return this;
    }
    // No callback provided? Return a Promise
    return new Promise(function(resolve, reject) {
        callback = function(err, data) {
            (err && reject(err)) || resolve(data);
        };
        prepareRequest();
    });
};

/**
 * Perform a GET request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.get = function(options, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(options, callback);
};

/**
 * Perform a POST request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {object} data - POST data
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.post = function(options, data, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(util.merge(options, {method: 'POST', data: data}), callback);
};

/**
 * Perform a PUT request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {object} data - PUT data
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.put = function(options, data, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(util.merge(options, {method: 'PUT', data: data}), callback);
};

/**
 * Perform a DELETE request against the Discogs API
 * @param {(object|string)} options - Request options object or an url
 * @param {function} [callback] - Callback function receiving the data
 * @return {DiscogsClient|Promise}
 */

DiscogsClient.prototype.delete = function(options, callback) {
    if (typeof options === 'string') {
        options = {url: options};
    }
    return this._request(util.merge(options, {method: 'DELETE'}), callback);
};

/**
 * Get an instance of the Discogs OAuth class
 * @return {DiscogsOAuth}
 */

DiscogsClient.prototype.oauth = function() {
    var OAuth = require('./oauth.js');
    return new OAuth(this.auth);
};

/**
 * Expose the database functions and pass the current instance
 * @return {object}
 */

DiscogsClient.prototype.database = function() {
    return require('./database.js')(this);
};

/**
 * Expose the marketplace functions and pass the current instance
 * @return {object}
 */

DiscogsClient.prototype.marketplace = function() {
    return require('./marketplace.js')(this);
};

/**
 * Expose the user functions and pass the current instance
 * @return {object}
 */

DiscogsClient.prototype.user = function() {
    return require('./user.js')(this);
};
<|endoftext|>


<commit_before>'use strict';

var request = require('request');
var constants = require('./constants');

var ALLOWED_CREDENTIALS = [
	{ name: 'userAuthKey', type: 'string' },
	{ name: 'app', type: 'object', requiredFields: ['appAuthKey', 'appId'] },
	{ name: 'apps', type: 'object'}
];

/**
 * make a basic request
 * @param url
 * @param apiKey
 * @param method :  [ GET, POST, PUT ...]
 * @param body
 * @param callback  (err, httpResponse, body)
 */
var basicRequest = function (url, apiKey, method, body, callback) {
	var options = {
		url: url,
		method: method
	};
	if (apiKey) {
		options.headers = {
			'Content-Type': 'application/json; charset=utf-8',
			'Authorization': 'Basic ' + apiKey
		}
	}
	if (body) {
		options.body = body;
		options.json = true;
  }

	return new Promise(function (resolve, reject) {
    request(options, function (err, httpResponse, data) {
      if (err) {
        callback && callback(err, httpResponse, data);
        return reject(err);
      }

      callback && callback(err, httpResponse, data);
      return resolve({ httpResponse: httpResponse, data: data });
    });
  });
};

/**
 * check if the credential is valid
 * @param credentialName
 * @param credential
 * @returns {boolean}
 */
var checkCredential = function (credentialName, credential) {
	for (var i = 0, credentialLen = ALLOWED_CREDENTIALS.length; i < credentialLen; i++) {
		if (ALLOWED_CREDENTIALS[i].name === credentialName) {
      if (typeof credential !== ALLOWED_CREDENTIALS[i].type) {
				throw credentialName + ' must be a ' + ALLOWED_CREDENTIALS[i].type;
			}
			if (ALLOWED_CREDENTIALS[i].requiredFields) {
				for (var j = 0; j < ALLOWED_CREDENTIALS[i].requiredFields.length; j++) {
					if (!(ALLOWED_CREDENTIALS[i].requiredFields[j] in credential)) {
						throw credentialName + ' must contain ' +  ALLOWED_CREDENTIALS[i].requiredFields[j]
					}
				}
			}
			return true;
		}
	}
	return false;
};

/**
 *
 * @param credentials { JSON }
 * @constructor
 */
var Client = function (credentials) {
	if (typeof credentials !== 'object') {
		throw 'credentials parameter must be a JSON object'
	}
	this.API_URI = constants.API_ROOT;
	for (var key in credentials) {
		if (credentials.hasOwnProperty(key) && checkCredential(key, credentials[key])) {
			this[key] = credentials[key];
		}
	}
};

/**
 *
 * @param rootUrl { String }   default 'https://onesignal.com/api/v1';
 */
Client.prototype.setRootUrl = function (rootUrl) {
	if (!rootUrl) {
		throw 'You must set a valid rootUsrl.'
	}
	this.API_URI = rootUrl;
};

/**
 * set an app object
 * @param app { JSON }  {}
 */
Client.prototype.setApp = function (app) {
	checkCredential('app', app);
	this.app = app;
};

/**
 * create a notification
 * @param notification { Notification }
 * @param callback
 */
Client.prototype.sendNotification = function (notification, callback) {
	if (!notification || !notification.postBody) {
		throw 'notification parameter must be a typeof Notification object.';
	}
	var postBody = notification.postBody;
	if (this.apps && this.apps.length > 0) {
		postBody.app_ids = this.apps;
		return basicRequest(this.API_URI + constants.NOTIFICATIONS_PATH, this.userAuthKey, 'POST', postBody, callback);
  }
  if (this.app) {
    postBody.app_id  = this.app.appId;
    return basicRequest(this.API_URI + constants.NOTIFICATIONS_PATH, this.app.appAuthKey, 'POST', postBody, callback);
  }
  throw 'You must set either an "app" or "apps" on Client';
};

/**
 * Used to stop a scheduled or currently outgoing notification.
 * @param notificationId	{ String }   Notification id
 * @param callback
 */
Client.prototype.cancelNotification = function (notificationId, callback) {
	if (!this.app) {
		throw 'You must define an "app" object.'
	}
	var notificationUri = this.API_URI + constants.NOTIFICATIONS_PATH + '/' + notificationId + '?app_id=' + this.app.appId;
  return basicRequest(notificationUri, this.app.appAuthKey, 'DELETE', null, callback);
};

/**
 *
 * @param notificationId { String }
 * @param callback
 */
Client.prototype.viewNotification = function (notificationId, callback) {
	if (!this.app) {
		throw 'You must define an "app" object.'
	}
	var notificationUri = this.API_URI + constants.NOTIFICATIONS_PATH + '/' + notificationId + '?app_id=' + this.app.appId;
  return basicRequest(notificationUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param query { String }  ex: limit:100&offset=9
 * @param callback
 */
Client.prototype.viewNotifications = function (query, callback) {
	if (!this.app) {
		throw 'You must define an "app" object.'
	}
	var appUri = this.API_URI + constants.NOTIFICATIONS_PATH + '?app_id=' + this.app.appId + '&' + query;
  return basicRequest(appUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param callback
 */
Client.prototype.viewApps = function (callback) {
	if (!this.userAuthKey) {
		throw 'You must define "userAuthKey" on Client'
	}
  return basicRequest(this.API_URI + constants.APPS_PATH, this.userAuthKey, 'GET', null, callback);
};

/**
 *
 * @param appId { String }
 * @param callback
 */
Client.prototype.viewApp = function (appId, callback) {
	if (!this.userAuthKey) {
		throw 'You must define "userAuthKey" on Client'
	}
  return basicRequest(this.API_URI + constants.APPS_PATH + '/' + appId, this.userAuthKey, 'GET', null, callback);
};

/**
 *
 * @param body { JSON }
 * @param callback
 */
Client.prototype.createApp = function (body, callback) {
	if (!body.name) {
		throw 'You must specify a name in body';
	}
    if (!this.userAuthKey) {
    throw 'You must define "userAuthKey" on Client'
  }
  return basicRequest(this.API_URI + constants.APPS_PATH, this.userAuthKey, 'POST', body, callback);
};

/**
 * Updates currently defined app
 * @param body	{ JSON }
 * @param callback
 */
Client.prototype.updateApp = function (body, callback) {
	if (!this.app) {
		throw 'You must define an "app" object.'
	}
  if (!this.userAuthKey) {
    throw 'You must define "userAuthKey" on Client'
  }
  return basicRequest(this.API_URI + constants.APPS_PATH + '/' + this.app.appId, this.userAuthKey, 'PUT', body, callback);
};

/**
 *
 * @param query  { String }  ex: limit=200&offset=10
 * @param callback
 */
Client.prototype.viewDevices = function (query, callback) {
	if (!this.app) {
		throw 'You must define an "app" object.'
	}
	var viewUri = this.API_URI + constants.DEVICES_PATH + '?app_id=' + this.app.appId + '&' + query;
  return basicRequest(viewUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param deviceId { String }
 * @param callback
 */
Client.prototype.viewDevice = function (deviceId, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var viewUri = this.API_URI + constants.DEVICES_PATH + '/' + deviceId + '?app_id=' + this.app.appId;
  return basicRequest(viewUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param body { JSON }
 * @param callback
 */
Client.prototype.addDevice = function (body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  if (!('app_id' in body)) {
    body.app_id = this.app.appId;
  }
  return basicRequest(this.API_URI + constants.DEVICES_PATH, this.app.appAuthKey, 'POST', body, callback);
};

/**
 *
 * @param deviceId { String }
 * @param body { JSON }
 * @param callback
 */
Client.prototype.editDevice = function (deviceId, body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  return basicRequest(this.API_URI + constants.DEVICES_PATH + '/' + deviceId, this.app.appAuthKey, 'PUT', body, callback);
};

/**
 *
 * @param notificationId { String }
 * @param body { JSON }
 * @param callback
 */
Client.prototype.trackOpen = function (notificationId, body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  if (!('app_id' in body)) {
    body.app_id = this.app.appId;
  }
  return basicRequest(this.API_URI + constants.NOTIFICATIONS_PATH + '/' + notificationId, this.app.appAuthKey, 'PUT', body, callback);
};

/**
 *
 * @param body
 * @param callback
 */
Client.prototype.csvExport = function (body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var csvUri = this.API_URI + constants.DEVICES_PATH + '/csv_export' + '?app_id=' + this.app.appId;
	return basicRequest(csvUri, this.app.appAuthKey, 'POST', body, callback);
};

module.exports = Client;
<commit_msg>Change kind of indentation<commit_after>'use strict';

var request = require('request');
var constants = require('./constants');

var ALLOWED_CREDENTIALS = [
  { name: 'userAuthKey', type: 'string' },
  { name: 'app', type: 'object', requiredFields: ['appAuthKey', 'appId'] },
  { name: 'apps', type: 'object'}
];

/**
 * make a basic request
 * @param url
 * @param apiKey
 * @param method :  [ GET, POST, PUT ...]
 * @param body
 * @param callback  (err, httpResponse, body)
 */
var basicRequest = function (url, apiKey, method, body, callback) {
  var options = {
    url: url,
    method: method
  };
  if (apiKey) {
    options.headers = {
      'Content-Type': 'application/json; charset=utf-8',
      'Authorization': 'Basic ' + apiKey
    }
  }
  if (body) {
    options.body = body;
    options.json = true;
  }

  return new Promise(function (resolve, reject) {
    request(options, function (err, httpResponse, data) {
      if (err) {
        callback && callback(err, httpResponse, data);
        return reject(err);
      }

      callback && callback(err, httpResponse, data);
      return resolve({ httpResponse: httpResponse, data: data });
    });
  });
};

/**
 * check if the credential is valid
 * @param credentialName
 * @param credential
 * @returns {boolean}
 */
var checkCredential = function (credentialName, credential) {
  for (var i = 0, credentialLen = ALLOWED_CREDENTIALS.length; i < credentialLen; i++) {
    if (ALLOWED_CREDENTIALS[i].name === credentialName) {
      if (typeof credential !== ALLOWED_CREDENTIALS[i].type) {
        throw credentialName + ' must be a ' + ALLOWED_CREDENTIALS[i].type;
      }
      if (ALLOWED_CREDENTIALS[i].requiredFields) {
        for (var j = 0; j < ALLOWED_CREDENTIALS[i].requiredFields.length; j++) {
          if (!(ALLOWED_CREDENTIALS[i].requiredFields[j] in credential)) {
            throw credentialName + ' must contain ' +  ALLOWED_CREDENTIALS[i].requiredFields[j]
          }
        }
      }
      return true;
    }
  }
  return false;
};

/**
 *
 * @param credentials { JSON }
 * @constructor
 */
var Client = function (credentials) {
  if (typeof credentials !== 'object') {
    throw 'credentials parameter must be a JSON object'
  }
  this.API_URI = constants.API_ROOT;
  for (var key in credentials) {
    if (credentials.hasOwnProperty(key) && checkCredential(key, credentials[key])) {
      this[key] = credentials[key];
    }
  }
};

/**
 *
 * @param rootUrl { String }   default 'https://onesignal.com/api/v1';
 */
Client.prototype.setRootUrl = function (rootUrl) {
  if (!rootUrl) {
    throw 'You must set a valid rootUsrl.'
  }
  this.API_URI = rootUrl;
};

/**
 * set an app object
 * @param app { JSON }  {}
 */
Client.prototype.setApp = function (app) {
  checkCredential('app', app);
  this.app = app;
};

/**
 * create a notification
 * @param notification { Notification }
 * @param callback
 */
Client.prototype.sendNotification = function (notification, callback) {
  if (!notification || !notification.postBody) {
    throw 'notification parameter must be a typeof Notification object.';
  }
  var postBody = notification.postBody;
  if (this.apps && this.apps.length > 0) {
    postBody.app_ids = this.apps;
    return basicRequest(this.API_URI + constants.NOTIFICATIONS_PATH, this.userAuthKey, 'POST', postBody, callback);
  }
  if (this.app) {
    postBody.app_id  = this.app.appId;
    return basicRequest(this.API_URI + constants.NOTIFICATIONS_PATH, this.app.appAuthKey, 'POST', postBody, callback);
  }
  throw 'You must set either an "app" or "apps" on Client';
};

/**
 * Used to stop a scheduled or currently outgoing notification.
 * @param notificationId	{ String }   Notification id
 * @param callback
 */
Client.prototype.cancelNotification = function (notificationId, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var notificationUri = this.API_URI + constants.NOTIFICATIONS_PATH + '/' + notificationId + '?app_id=' + this.app.appId;
  return basicRequest(notificationUri, this.app.appAuthKey, 'DELETE', null, callback);
};

/**
 *
 * @param notificationId { String }
 * @param callback
 */
Client.prototype.viewNotification = function (notificationId, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var notificationUri = this.API_URI + constants.NOTIFICATIONS_PATH + '/' + notificationId + '?app_id=' + this.app.appId;
  return basicRequest(notificationUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param query { String }  ex: limit:100&offset=9
 * @param callback
 */
Client.prototype.viewNotifications = function (query, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var appUri = this.API_URI + constants.NOTIFICATIONS_PATH + '?app_id=' + this.app.appId + '&' + query;
  return basicRequest(appUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param callback
 */
Client.prototype.viewApps = function (callback) {
  if (!this.userAuthKey) {
    throw 'You must define "userAuthKey" on Client'
  }
  return basicRequest(this.API_URI + constants.APPS_PATH, this.userAuthKey, 'GET', null, callback);
};

/**
 *
 * @param appId { String }
 * @param callback
 */
Client.prototype.viewApp = function (appId, callback) {
  if (!this.userAuthKey) {
    throw 'You must define "userAuthKey" on Client'
  }
  return basicRequest(this.API_URI + constants.APPS_PATH + '/' + appId, this.userAuthKey, 'GET', null, callback);
};

/**
 *
 * @param body { JSON }
 * @param callback
 */
Client.prototype.createApp = function (body, callback) {
  if (!body.name) {
    throw 'You must specify a name in body';
  }
    if (!this.userAuthKey) {
    throw 'You must define "userAuthKey" on Client'
  }
  return basicRequest(this.API_URI + constants.APPS_PATH, this.userAuthKey, 'POST', body, callback);
};

/**
 * Updates currently defined app
 * @param body	{ JSON }
 * @param callback
 */
Client.prototype.updateApp = function (body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  if (!this.userAuthKey) {
    throw 'You must define "userAuthKey" on Client'
  }
  return basicRequest(this.API_URI + constants.APPS_PATH + '/' + this.app.appId, this.userAuthKey, 'PUT', body, callback);
};

/**
 *
 * @param query  { String }  ex: limit=200&offset=10
 * @param callback
 */
Client.prototype.viewDevices = function (query, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var viewUri = this.API_URI + constants.DEVICES_PATH + '?app_id=' + this.app.appId + '&' + query;
  return basicRequest(viewUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param deviceId { String }
 * @param callback
 */
Client.prototype.viewDevice = function (deviceId, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var viewUri = this.API_URI + constants.DEVICES_PATH + '/' + deviceId + '?app_id=' + this.app.appId;
  return basicRequest(viewUri, this.app.appAuthKey, 'GET', null, callback);
};

/**
 *
 * @param body { JSON }
 * @param callback
 */
Client.prototype.addDevice = function (body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  if (!('app_id' in body)) {
    body.app_id = this.app.appId;
  }
  return basicRequest(this.API_URI + constants.DEVICES_PATH, this.app.appAuthKey, 'POST', body, callback);
};

/**
 *
 * @param deviceId { String }
 * @param body { JSON }
 * @param callback
 */
Client.prototype.editDevice = function (deviceId, body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  return basicRequest(this.API_URI + constants.DEVICES_PATH + '/' + deviceId, this.app.appAuthKey, 'PUT', body, callback);
};

/**
 *
 * @param notificationId { String }
 * @param body { JSON }
 * @param callback
 */
Client.prototype.trackOpen = function (notificationId, body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  if (!('app_id' in body)) {
    body.app_id = this.app.appId;
  }
  return basicRequest(this.API_URI + constants.NOTIFICATIONS_PATH + '/' + notificationId, this.app.appAuthKey, 'PUT', body, callback);
};

/**
 *
 * @param body
 * @param callback
 */
Client.prototype.csvExport = function (body, callback) {
  if (!this.app) {
    throw 'You must define an "app" object.'
  }
  var csvUri = this.API_URI + constants.DEVICES_PATH + '/csv_export' + '?app_id=' + this.app.appId;
  return basicRequest(csvUri, this.app.appAuthKey, 'POST', body, callback);
};

module.exports = Client;
<|endoftext|>


<commit_before>var php = require('phpjs');
var net = require('net');
var paredit = require('paredit.js');

/* Swank client class! */

function Client(host, port) {
  this.host = host;
  this.port = port;
  this.socket = null;
  this.connected = false;

  // Useful protocol information
  this.req_counter = 1;
  this.req_table = {};

  this.on_handlers = {
    connect: function() {},
    print_string: function(m) {},
    presentation_start: function () {},
    presentation_end: function () {},
    new_package: function(p) {},
    debug_activate: function(obj) {},
    debug_setup: function(obj) {},
    debug_return: function(obj) {},
    disconnect: function() {}
  }

  // Bootstrap the reading state
  this.setup_read(6, this.header_complete_callback);
}

/*****************************************************************
 Low-level data handling protocol
 *****************************************************************/

Client.prototype.send_message = function(msg) {
  var msg_utf8 = php.utf8_encode(msg);
  // Construct the length, which is a 6-byte
  // hexadecimal length string
  var len_str = msg_utf8.length.toString(16);
  len_str = Array((6 - len_str.length) + 1).join('0') + len_str;
  // Assemble overall message
  var msg_overall = len_str + msg_utf8;
  // Send it
  // console.log("Write:")
  // console.log("    Length: " + len_str + " (" + msg_utf8.length + ")");
  // console.log("    Msg: ...");
  // console.log(msg_overall)
  this.socket.write(msg_overall);
}

Client.prototype.connect = function() {
  var sc = this; // Because the 'this' operator changes scope
  return new Promise(function(resolve, reject) {
    // Create a socket
    sc.socket = net.connect({
        host: sc.host,
        port: sc.port
      }, function() {
        sc.connected = true;
        resolve();
      });
    sc.socket.setNoDelay(true);
    sc.socket.setEncoding('ascii');

    sc.socket.on('error', function(err) {
      reject();
    })
    sc.socket.on('data', function(data) {
      sc.socket_data_handler(data);
    });
    sc.socket.on('end', function() {
      sc.connected = false;
      sc.on_handlers.disconnect();
    });
  });
}

/* Some data just came in over the wire. Make sure to read it in
message chunks with the length */
Client.prototype.socket_data_handler = function(data) {
  var d = data.toString();

  while (d.length > 0) {
    if (d.length >= this.len_remaining) {
      // We can finish this buffer! Read the remaining
      // and reduce from d.
      // console.log("Consuming " + this.len_remaining + " of " + d.length);
      this.buffer += d.slice(0, this.len_remaining);
      d = d.slice(this.len_remaining);
      this.len_remaining = 0;
    } else {
      // We haven't read enough to complete the desired
      // read length yet. Consume it and update.
      // console.log("Consuming all " + d.length);
      this.buffer += d;
      this.len_remaining = this.len_remaining - d.length;
      d = "";
    }

    // If we've finished reading the entire buffer, call the callback!
    // console.log("Len Remaining: " + this.len_remaining);
    if (this.len_remaining == 0) {
        // console.log("   Buffer complete!");
        // console.log("   d left: " + d.length);
        this.buffer_complete_callback(this.buffer);
    }
  }
}

Client.prototype.setup_read = function(length, fn) {
  this.buffer = "";
  this.len_remaining = length;
  this.buffer_complete_callback = fn;
}

Client.prototype.header_complete_callback = function(data) {
  // Parse the length
  var len = parseInt(data, 16);
  // Set up to read data
  this.setup_read(len, this.data_complete_callback);
}

Client.prototype.data_complete_callback = function(data) {
  // Call the handler
  this.on_swank_message(php.utf8_decode(data))

  // Set up again to read the header
  this.setup_read(6, this.header_complete_callback); // It's 6 bytes long
}

Client.prototype.on = function(event, fn) {
  this.on_handlers[event] = fn;
}

Client.prototype.on_swank_message = function(msg) {
  // console.log(msg);
  var ast = paredit.parse(msg);
  var sexp = ast.children[0];
  var cmd = sexp.children[0].source.toLowerCase();
  if (cmd == ":return") {
    this.swank_message_rex_return_handler(sexp);
  } else if (cmd == ':write-string') {
    this.on_handlers.print_string(sexp.children[1].source.slice(1,-1));
  } else if (cmd == ':presentation-start') {
    this.on_handlers.presentation_start();
  } else if (cmd == ':presentation-end') {
    this.on_handlers.presentation_end();
  } else if (cmd == ":new-package") {
    this.on_handlers.new_package(sexp.children[1].source.slice(1, -1));
  } else if (cmd == ":debug") {
    this.debug_setup_handler(sexp);
  } else if (cmd == ":debug-activate") {
    this.debug_activate_handler(sexp);
  } else if (cmd == ":debug-return") {
    this.debug_return_handler(sexp);
  } else if (cmd == ":ping") {
    this.ping_handler(sexp);
  } else {
    // console.log("Ignoring command " + cmd);
  }

}

/*****************************************************************
 Evaluating EMACS-REX (remote execution) commandsreturn ast.children[0].children.map(function(competion) {
          return competion.source.slice(1, -1);
        })
 */

Client.prototype.rex = function(cmd, pkg, thread) {
  // Run an EMACS-REX command, and call the callback
  // when we have a return value, with the parsed paredit s-expression
  // Add an entry into our table!
  var sc = this;
  var resolve_fn = null;
  var id = sc.req_counter;
  var promise = new Promise(function(resolve, reject) {
    // Dispatch a command to swank
    resolve_fn = resolve;
    var rex_cmd = "(:EMACS-REX " + cmd + " \"" + pkg + "\" " + thread + " " + id + ")";
    // console.log(rex_cmd);
    sc.send_message(rex_cmd);
  });

  sc.req_counter = sc.req_counter + 1;
  sc.req_table[id] = {
    id: id,
    cmd: cmd,
    pkg: pkg,
    promise_resolve_fn: resolve_fn
  };
  return promise;
}

Client.prototype.swank_message_rex_return_handler = function(cmd) {
    var status = cmd.children[1].children[0].source.toLowerCase();
    var return_val = cmd.children[1].children[1];
    var id = cmd.children[2].source;

    // Look up the appropriate callback and return it!
    if (id in this.req_table) {
        var req = this.req_table[id];
        delete this.req_table[id];
        // console.log("Resolving " + id);
        req.promise_resolve_fn(return_val);
    } else {
        console.error("Received REX response for unknown command ID");
    }
}

Client.prototype.ping_handler = function(sexp) {
  // Swank occasionally send's ping messages to see if we're okay.
  // We must respond!
  var response = '(:EMACS-PONG ' + sexp.children[1].source + ' ' + sexp.children[2].source + ')';
  this.send_message(response);
}

/*****************************************************************
 Higher-level commands
 */
Client.prototype.initialize = function() {
  // Run these useful initialization commands one after another
  var self = this;
  return self.rex("(SWANK:SWANK-REQUIRE  \
    '(SWANK-IO-PACKAGE::SWANK-TRACE-DIALOG SWANK-IO-PACKAGE::SWANK-PACKAGE-FU \
      SWANK-IO-PACKAGE::SWANK-PRESENTATIONS SWANK-IO-PACKAGE::SWANK-FUZZY \
      SWANK-IO-PACKAGE::SWANK-FANCY-INSPECTOR SWANK-IO-PACKAGE::SWANK-C-P-C \
      SWANK-IO-PACKAGE::SWANK-ARGLISTS SWANK-IO-PACKAGE::SWANK-REPL))", 'COMMON-LISP-USER', 'T')
      .then(function(response) {
        return self.rex("(SWANK:INIT-PRESENTATIONS)", 'COMMON-LISP-USER', 'T');})
      .then(function(response) {
        return self.rex('(SWANK-REPL:CREATE-REPL NIL :CODING-SYSTEM "utf-8-unix")', 'COMMON-LISP-USER', 'T');});
}

/* Gets autodocumentation for the given sexp, given the cursor's position */
Client.prototype.autodoc = function(sexp_string, cursor_position, pkg) {
  var ast = paredit.parse(sexp_string);
  try {
    var forms = ast.children[0];
    var output_forms = [];
    var didCursor = false;
    for(var i = 0; i < forms.children.length; i++) {
      var form = forms.children[i];
      output_forms.push('"' + form.source + '"');
      if (cursor_position >= form.start && cursor_position <= form.end && !didCursor) {
        output_forms.push('SWANK::%CURSOR-MARKER%');
        didCursor = true;
        break;
      }
    }
    if (!didCursor) {
      output_forms.push('""');
      output_forms.push('SWANK::%CURSOR-MARKER%');
      didCursor = true;
    }
    var cmd = '(SWANK:AUTODOC \'('; // '"potato" SWANK::%CURSOR-MARKER%) :PRINT-RIGHT-MARGIN 80)';
    cmd += output_forms.join(' ');
    cmd += ') :PRINT-RIGHT-MARGIN 80)';
  } catch (e) {
    // Return a promise with nothing then
    console.log("Error constructing command: " + e.toString());
    return Promise.resolve({type: 'symbol', source: ':not-available'});
  }
  // Return a promise that will yield the result.
  return this.rex(cmd, pkg, ':REPL-THREAD')
    .then(function (ast) {
      try {
        return ast.children[0];
      } catch (e) {
        return {type: 'symbol', source: ':not-available'};
      }
    });
}

Client.prototype.autocomplete = function(prefix, pkg) {
  prefix = prefix.replace("\"", "\\\"");
  // TODO - do we need to escape the above prefix more?
  var cmd = '(SWANK:SIMPLE-COMPLETIONS "' + prefix + '" \'"' + pkg + '")';
  return this.rex(cmd, pkg, "T")
    .then(function (ast) {
      try {
        return ast.children[0].children.map(function(competion) {
          return competion.source.slice(1, -1);
        });
      } catch (e) {
        return [];
      }
    });
}

Client.prototype.eval = function(sexp_string, pkg) {
  var cmd = '(SWANK-REPL:LISTENER-EVAL "' + sexp_string.replace(/\"/g, '\\"') + '")';
  return this.rex(cmd, pkg, ':REPL-THREAD');
}

Client.prototype.debug_setup_handler = function(sexp) {
  var obj = {};
  obj.thread = sexp.children[1].source;
  obj.level = sexp.children[2].source;
  obj.title = sexp.children[3].children[0].source.slice(1, -1);
  obj.type = sexp.children[3].children[1].source.slice(1, -1);
  obj.restarts = [];
  sexp.children[4].children.forEach(function(restart_sexp) {
    obj.restarts.push({
      cmd: restart_sexp.children[0].source.slice(1, -1),
      description: restart_sexp.children[1].source.slice(1, -1)
    });
  });
// TODO: stack trace

  this.on_handlers.debug_setup(obj);
}

Client.prototype.debug_activate_handler = function(sexp) {
  var thread = sexp.children[1].source;
  var level = sexp.children[2].source;
  this.on_handlers.debug_activate({thread: thread, level: level});
}

Client.prototype.debug_return_handler = function(sexp) {
  var thread = sexp.children[1].source;
  var level = sexp.children[2].source;
  this.on_handlers.debug_return({thread: thread, level: level});
}

Client.prototype.debug_invoke_restart = function(level, restart, thread) {
  var cmd = '(SWANK:INVOKE-NTH-RESTART-FOR-EMACS ' + level + ' ' + restart + ')';
  return this.rex(cmd, 'COMMON-LISP-USER', thread);
}

/* Escape from all errors */
Client.prototype.debug_escape_all = function() {
  var cmd = '(SWANK:THROW-TO-TOPLEVEL)';
  return this.rex(cmd, "COMMON-LISP-USER", "1"); //# TODO - is "1" ok here?
}

// Returns function definitions. Returns a list of objects, each of which has a
// label property, a filename, and an index
Client.prototype.find_definitions = function(fn, pkg) {
  var cmd = '(SWANK:FIND-DEFINITIONS-FOR-EMACS "' + fn + '")';
  return this.rex(cmd, pkg, "T")
    .then(function (ast) {
      var refs = [];
      for(var i = 0; i < ast.children.length; i++) {
        try {
          refs.push({
            label: ast.children[i].children[0].source.slice(1, -1),
            filename: ast.children[i].children[1].children[1].children[1].source.slice(1, -1),
            index: parseInt(ast.children[i].children[1].children[2].children[1].source)
          });
        } catch (e) {
          // Don't add the reference - it didn't parse correctly
        }
      }
      return refs;
    });
}

Client.prototype.quit = function() {
  var cmd = '(SWANK/BACKEND:QUIT-LISP)';
  return this.rex(cmd, "COMMON-LISP-USER", "T")
}

module.exports.Client = Client;
<commit_msg>Added interrupt command<commit_after>var php = require('phpjs');
var net = require('net');
var paredit = require('paredit.js');

/* Swank client class! */

function Client(host, port) {
  this.host = host;
  this.port = port;
  this.socket = null;
  this.connected = false;

  // Useful protocol information
  this.req_counter = 1;
  this.req_table = {};

  this.on_handlers = {
    connect: function() {},
    print_string: function(m) {},
    presentation_start: function () {},
    presentation_end: function () {},
    new_package: function(p) {},
    debug_activate: function(obj) {},
    debug_setup: function(obj) {},
    debug_return: function(obj) {},
    disconnect: function() {}
  }

  // Bootstrap the reading state
  this.setup_read(6, this.header_complete_callback);
}

/*****************************************************************
 Low-level data handling protocol
 *****************************************************************/

Client.prototype.send_message = function(msg) {
  var msg_utf8 = php.utf8_encode(msg);
  // Construct the length, which is a 6-byte
  // hexadecimal length string
  var len_str = msg_utf8.length.toString(16);
  len_str = Array((6 - len_str.length) + 1).join('0') + len_str;
  // Assemble overall message
  var msg_overall = len_str + msg_utf8;
  // Send it
  // console.log("Write:")
  // console.log("    Length: " + len_str + " (" + msg_utf8.length + ")");
  // console.log("    Msg: ...");
  // console.log(msg_overall)
  this.socket.write(msg_overall);
}

Client.prototype.connect = function() {
  var sc = this; // Because the 'this' operator changes scope
  return new Promise(function(resolve, reject) {
    // Create a socket
    sc.socket = net.connect({
        host: sc.host,
        port: sc.port
      }, function() {
        sc.connected = true;
        resolve();
      });
    sc.socket.setNoDelay(true);
    sc.socket.setEncoding('ascii');

    sc.socket.on('error', function(err) {
      reject();
    })
    sc.socket.on('data', function(data) {
      sc.socket_data_handler(data);
    });
    sc.socket.on('end', function() {
      sc.connected = false;
      sc.on_handlers.disconnect();
    });
  });
}

/* Some data just came in over the wire. Make sure to read it in
message chunks with the length */
Client.prototype.socket_data_handler = function(data) {
  var d = data.toString();

  while (d.length > 0) {
    if (d.length >= this.len_remaining) {
      // We can finish this buffer! Read the remaining
      // and reduce from d.
      // console.log("Consuming " + this.len_remaining + " of " + d.length);
      this.buffer += d.slice(0, this.len_remaining);
      d = d.slice(this.len_remaining);
      this.len_remaining = 0;
    } else {
      // We haven't read enough to complete the desired
      // read length yet. Consume it and update.
      // console.log("Consuming all " + d.length);
      this.buffer += d;
      this.len_remaining = this.len_remaining - d.length;
      d = "";
    }

    // If we've finished reading the entire buffer, call the callback!
    // console.log("Len Remaining: " + this.len_remaining);
    if (this.len_remaining == 0) {
        // console.log("   Buffer complete!");
        // console.log("   d left: " + d.length);
        this.buffer_complete_callback(this.buffer);
    }
  }
}

Client.prototype.setup_read = function(length, fn) {
  this.buffer = "";
  this.len_remaining = length;
  this.buffer_complete_callback = fn;
}

Client.prototype.header_complete_callback = function(data) {
  // Parse the length
  var len = parseInt(data, 16);
  // Set up to read data
  this.setup_read(len, this.data_complete_callback);
}

Client.prototype.data_complete_callback = function(data) {
  // Call the handler
  this.on_swank_message(php.utf8_decode(data))

  // Set up again to read the header
  this.setup_read(6, this.header_complete_callback); // It's 6 bytes long
}

Client.prototype.on = function(event, fn) {
  this.on_handlers[event] = fn;
}

Client.prototype.on_swank_message = function(msg) {
  // console.log(msg);
  var ast = paredit.parse(msg);
  var sexp = ast.children[0];
  var cmd = sexp.children[0].source.toLowerCase();
  if (cmd == ":return") {
    this.swank_message_rex_return_handler(sexp);
  } else if (cmd == ':write-string') {
    this.on_handlers.print_string(sexp.children[1].source.slice(1,-1));
  } else if (cmd == ':presentation-start') {
    this.on_handlers.presentation_start();
  } else if (cmd == ':presentation-end') {
    this.on_handlers.presentation_end();
  } else if (cmd == ":new-package") {
    this.on_handlers.new_package(sexp.children[1].source.slice(1, -1));
  } else if (cmd == ":debug") {
    this.debug_setup_handler(sexp);
  } else if (cmd == ":debug-activate") {
    this.debug_activate_handler(sexp);
  } else if (cmd == ":debug-return") {
    this.debug_return_handler(sexp);
  } else if (cmd == ":ping") {
    this.ping_handler(sexp);
  } else {
    // console.log("Ignoring command " + cmd);
  }

}

/*****************************************************************
 Evaluating EMACS-REX (remote execution) commandsreturn ast.children[0].children.map(function(competion) {
          return competion.source.slice(1, -1);
        })
 */

Client.prototype.rex = function(cmd, pkg, thread) {
  // Run an EMACS-REX command, and call the callback
  // when we have a return value, with the parsed paredit s-expression
  // Add an entry into our table!
  var sc = this;
  var resolve_fn = null;
  var id = sc.req_counter;
  var promise = new Promise(function(resolve, reject) {
    // Dispatch a command to swank
    resolve_fn = resolve;
    var rex_cmd = "(:EMACS-REX " + cmd + " \"" + pkg + "\" " + thread + " " + id + ")";
    // console.log(rex_cmd);
    sc.send_message(rex_cmd);
  });

  sc.req_counter = sc.req_counter + 1;
  sc.req_table[id] = {
    id: id,
    cmd: cmd,
    pkg: pkg,
    promise_resolve_fn: resolve_fn
  };
  return promise;
}

Client.prototype.swank_message_rex_return_handler = function(cmd) {
    var status = cmd.children[1].children[0].source.toLowerCase();
    var return_val = cmd.children[1].children[1];
    var id = cmd.children[2].source;

    // Look up the appropriate callback and return it!
    if (id in this.req_table) {
        var req = this.req_table[id];
        delete this.req_table[id];
        // console.log("Resolving " + id);
        req.promise_resolve_fn(return_val);
    } else {
        console.error("Received REX response for unknown command ID");
    }
}

Client.prototype.ping_handler = function(sexp) {
  // Swank occasionally send's ping messages to see if we're okay.
  // We must respond!
  var response = '(:EMACS-PONG ' + sexp.children[1].source + ' ' + sexp.children[2].source + ')';
  this.send_message(response);
}

/*****************************************************************
 Higher-level commands
 */
Client.prototype.initialize = function() {
  // Run these useful initialization commands one after another
  var self = this;
  return self.rex("(SWANK:SWANK-REQUIRE  \
    '(SWANK-IO-PACKAGE::SWANK-TRACE-DIALOG SWANK-IO-PACKAGE::SWANK-PACKAGE-FU \
      SWANK-IO-PACKAGE::SWANK-PRESENTATIONS SWANK-IO-PACKAGE::SWANK-FUZZY \
      SWANK-IO-PACKAGE::SWANK-FANCY-INSPECTOR SWANK-IO-PACKAGE::SWANK-C-P-C \
      SWANK-IO-PACKAGE::SWANK-ARGLISTS SWANK-IO-PACKAGE::SWANK-REPL))", 'COMMON-LISP-USER', 'T')
      .then(function(response) {
        return self.rex("(SWANK:INIT-PRESENTATIONS)", 'COMMON-LISP-USER', 'T');})
      .then(function(response) {
        return self.rex('(SWANK-REPL:CREATE-REPL NIL :CODING-SYSTEM "utf-8-unix")', 'COMMON-LISP-USER', 'T');});
}

/* Gets autodocumentation for the given sexp, given the cursor's position */
Client.prototype.autodoc = function(sexp_string, cursor_position, pkg) {
  var ast = paredit.parse(sexp_string);
  try {
    var forms = ast.children[0];
    var output_forms = [];
    var didCursor = false;
    for(var i = 0; i < forms.children.length; i++) {
      var form = forms.children[i];
      output_forms.push('"' + form.source + '"');
      if (cursor_position >= form.start && cursor_position <= form.end && !didCursor) {
        output_forms.push('SWANK::%CURSOR-MARKER%');
        didCursor = true;
        break;
      }
    }
    if (!didCursor) {
      output_forms.push('""');
      output_forms.push('SWANK::%CURSOR-MARKER%');
      didCursor = true;
    }
    var cmd = '(SWANK:AUTODOC \'('; // '"potato" SWANK::%CURSOR-MARKER%) :PRINT-RIGHT-MARGIN 80)';
    cmd += output_forms.join(' ');
    cmd += ') :PRINT-RIGHT-MARGIN 80)';
  } catch (e) {
    // Return a promise with nothing then
    console.log("Error constructing command: " + e.toString());
    return Promise.resolve({type: 'symbol', source: ':not-available'});
  }
  // Return a promise that will yield the result.
  return this.rex(cmd, pkg, ':REPL-THREAD')
    .then(function (ast) {
      try {
        return ast.children[0];
      } catch (e) {
        return {type: 'symbol', source: ':not-available'};
      }
    });
}

Client.prototype.autocomplete = function(prefix, pkg) {
  prefix = prefix.replace("\"", "\\\"");
  // TODO - do we need to escape the above prefix more?
  var cmd = '(SWANK:SIMPLE-COMPLETIONS "' + prefix + '" \'"' + pkg + '")';
  return this.rex(cmd, pkg, "T")
    .then(function (ast) {
      try {
        return ast.children[0].children.map(function(competion) {
          return competion.source.slice(1, -1);
        });
      } catch (e) {
        return [];
      }
    });
}

Client.prototype.eval = function(sexp_string, pkg) {
  var cmd = '(SWANK-REPL:LISTENER-EVAL "' + sexp_string.replace(/\"/g, '\\"') + '")';
  return this.rex(cmd, pkg, ':REPL-THREAD');
}

Client.prototype.debug_setup_handler = function(sexp) {
  var obj = {};
  obj.thread = sexp.children[1].source;
  obj.level = sexp.children[2].source;
  obj.title = sexp.children[3].children[0].source.slice(1, -1);
  obj.type = sexp.children[3].children[1].source.slice(1, -1);
  obj.restarts = [];
  sexp.children[4].children.forEach(function(restart_sexp) {
    obj.restarts.push({
      cmd: restart_sexp.children[0].source.slice(1, -1),
      description: restart_sexp.children[1].source.slice(1, -1)
    });
  });
// TODO: stack trace

  this.on_handlers.debug_setup(obj);
}

Client.prototype.debug_activate_handler = function(sexp) {
  var thread = sexp.children[1].source;
  var level = sexp.children[2].source;
  this.on_handlers.debug_activate({thread: thread, level: level});
}

Client.prototype.debug_return_handler = function(sexp) {
  var thread = sexp.children[1].source;
  var level = sexp.children[2].source;
  this.on_handlers.debug_return({thread: thread, level: level});
}

Client.prototype.debug_invoke_restart = function(level, restart, thread) {
  var cmd = '(SWANK:INVOKE-NTH-RESTART-FOR-EMACS ' + level + ' ' + restart + ')';
  return this.rex(cmd, 'COMMON-LISP-USER', thread);
}

/* Escape from all errors */
Client.prototype.debug_escape_all = function() {
  var cmd = '(SWANK:THROW-TO-TOPLEVEL)';
  return this.rex(cmd, "COMMON-LISP-USER", "1"); //# TODO - is "1" ok here?
}

// Returns function definitions. Returns a list of objects, each of which has a
// label property, a filename, and an index
Client.prototype.find_definitions = function(fn, pkg) {
  var cmd = '(SWANK:FIND-DEFINITIONS-FOR-EMACS "' + fn + '")';
  return this.rex(cmd, pkg, "T")
    .then(function (ast) {
      var refs = [];
      for(var i = 0; i < ast.children.length; i++) {
        try {
          refs.push({
            label: ast.children[i].children[0].source.slice(1, -1),
            filename: ast.children[i].children[1].children[1].children[1].source.slice(1, -1),
            index: parseInt(ast.children[i].children[1].children[2].children[1].source)
          });
        } catch (e) {
          // Don't add the reference - it didn't parse correctly
        }
      }
      return refs;
    });
}

Client.prototype.interrupt = function() {
  var cmd = '(:EMACS-INTERRUPT :REPL-THREAD)';
  this.send_message(cmd);
}

Client.prototype.quit = function() {
  var cmd = '(SWANK/BACKEND:QUIT-LISP)';
  return this.rex(cmd, "COMMON-LISP-USER", "T")
}

module.exports.Client = Client;
<|endoftext|>


<commit_before>// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala

var ot_client = (function (global) {

  var operational_transformation = global.operational_transformation || require('./operational-transformation');

  function makeStateMachine (Ctor, states) {
    Ctor.prototype.callMethodForState = function (method) {
      var args = Array.prototype.slice.call(arguments, 1);
      return states[this.state][method].apply(this, args);
    };

    Ctor.prototype.transitionTo = function (name) {
      var args = Array.prototype.slice.call(arguments, 1);
      states[this.state].exit.apply(this, []);
      states[this.state = name].enter.apply(this, args);
    };
  }

  function extend (target, source) {
    for (var name in source) {
      if (source.hasOwnProperty(name)) {
        target[name] = source[name];
      }
    }
  }

  function assert (b, msg) {
    if (!b) {
      throw new Error(msg || "assertion error");
    }
  }

  function Client (revision) {
    assert(typeof revision === 'number' && revision >= 0);
    this.serverRevision = revision;
    this.state = 'synchronized';
  }

  Client.extend = function () {};

  Client.prototype.createOperation = function () {
    return new operational_transformation.Operation(this.callMethodForState('newRevision'));
  };

  Client.prototype.applyClient = function (operation) {
    return this.callMethodForState('applyClient', operation);
  };

  Client.prototype.applyServer = function (operation) {
    assert(operation.revision === this.serverRevision);
    this.callMethodForState('applyServer', operation);
    this.serverRevision++;
  };

  Client.prototype.sendOperation = function (operation) {
    throw new Error("sendOperation must be defined in child class");
  };

  Client.prototype.applyOperation = function (operation) {
    throw new Error("applyOperation must be defined in child class");
  };

  makeStateMachine(Client, {
    synchronized: {
      enter: function () {},
      exit: function () {},
      applyClient: function (operation) {
        this.sendOperation(operation);
        this.transitionTo('awaitingConfirm', operation);
      },
      applyServer: function (operation) {
        this.applyOperation(operation);
      },
      newRevision: function () {
        return this.serverRevision;
      }
    },

    awaitingConfirm: {
      enter: function (outstanding) {
        this.outstanding = outstanding;
      },
      exit: function () {
        delete this.outstanding;
      },
      applyClient: function (operation) {
        assert(operation.revision === this.serverRevision + 1);
        this.transitionTo('awaitingWithBuffer', this.outstanding, operation);
      },
      applyServer: function (operation) {
        if (operation.id === this.outstanding.id) {
          this.transitionTo('synchronized');
        } else {
          var pair = operational_transformation.transform(this.outstanding, operation);
          this.applyOperation(pair[1]);
          this.outstanding = pair[0];
        }
      },
      newRevision: function () {
        return this.serverRevision + 1;
      }
    },

    awaitingWithBuffer: {
      enter: function (outstanding, buffer) {
        this.outstanding = outstanding;
        this.buffer = buffer;
      },
      exit: function () {
        delete this.outstanding;
        delete this.buffer;
      },
      applyClient: function (operation) {
        assert(operation.revision === this.serverRevision + 2);
        this.buffer = operational_transformation.compose(this.buffer, operation);
      },
      applyServer: function (operation) {
        if (operation.id === this.outstanding.id) {
          this.sendOperation(this.buffer);
          this.transitionTo('awaitingConfirm', this.buffer);
        } else {
          var pair1 = operational_transformation.transform(this.outstanding, operation);
          this.outstanding = pair1[0];
          var operationPrime = pair1[1];
          var pair2 = operational_transformation.transform(this.buffer, operationPrime);
          this.buffer = pair2[0];
          this.applyOperation(pair2[1]);
        }
      },
      newRevision: function () {
        return this.serverRevision + 2;
      }
    }
  });

  return {
    Client: Client
  };

})(this);

if (typeof module === 'object') {
  module.exports = ot_client;
}<commit_msg>[client] refactor state machine<commit_after>// translation of https://github.com/djspiewak/cccp/blob/master/agent/src/main/scala/com/codecommit/cccp/agent/state.scala

var ot_client = (function (global) {

  var operational_transformation = global.operational_transformation || require('./operational-transformation');

  var StateMachine = {
    callMethodForState: function (method) {
      var args = Array.prototype.slice.call(arguments, 1);
      return this.states[this.state][method].apply(this, args);
    },

    transitionTo: function (name) {
      var args = Array.prototype.slice.call(arguments, 1);
      this.states[this.state].exit.apply(this, []);
      this.states[this.state = name].enter.apply(this, args);
    }
  };

  function extend (target, source) {
    for (var name in source) {
      if (source.hasOwnProperty(name)) {
        target[name] = source[name];
      }
    }
  }

  function assert (b, msg) {
    if (!b) {
      throw new Error(msg || "assertion error");
    }
  }

  function Client (revision) {
    assert(typeof revision === 'number' && revision >= 0);
    this.serverRevision = revision;
    this.state = 'synchronized';
  }

  extend(Client.prototype, StateMachine);

  Client.prototype.createOperation = function () {
    return new operational_transformation.Operation(this.callMethodForState('newRevision'));
  };

  Client.prototype.applyClient = function (operation) {
    return this.callMethodForState('applyClient', operation);
  };

  Client.prototype.applyServer = function (operation) {
    assert(operation.revision === this.serverRevision);
    this.callMethodForState('applyServer', operation);
    this.serverRevision++;
  };

  Client.prototype.sendOperation = function (operation) {
    throw new Error("sendOperation must be defined in child class");
  };

  Client.prototype.applyOperation = function (operation) {
    throw new Error("applyOperation must be defined in child class");
  };

  Client.prototype.states = {
    synchronized: {
      enter: function () {},
      exit: function () {},
      applyClient: function (operation) {
        this.sendOperation(operation);
        this.transitionTo('awaitingConfirm', operation);
      },
      applyServer: function (operation) {
        this.applyOperation(operation);
      },
      newRevision: function () {
        return this.serverRevision;
      }
    },

    awaitingConfirm: {
      enter: function (outstanding) {
        this.outstanding = outstanding;
      },
      exit: function () {
        delete this.outstanding;
      },
      applyClient: function (operation) {
        assert(operation.revision === this.serverRevision + 1);
        this.transitionTo('awaitingWithBuffer', this.outstanding, operation);
      },
      applyServer: function (operation) {
        if (operation.id === this.outstanding.id) {
          this.transitionTo('synchronized');
        } else {
          var pair = operational_transformation.transform(this.outstanding, operation);
          this.applyOperation(pair[1]);
          this.outstanding = pair[0];
        }
      },
      newRevision: function () {
        return this.serverRevision + 1;
      }
    },

    awaitingWithBuffer: {
      enter: function (outstanding, buffer) {
        this.outstanding = outstanding;
        this.buffer = buffer;
      },
      exit: function () {
        delete this.outstanding;
        delete this.buffer;
      },
      applyClient: function (operation) {
        assert(operation.revision === this.serverRevision + 2);
        this.buffer = operational_transformation.compose(this.buffer, operation);
      },
      applyServer: function (operation) {
        if (operation.id === this.outstanding.id) {
          this.sendOperation(this.buffer);
          this.transitionTo('awaitingConfirm', this.buffer);
        } else {
          var pair1 = operational_transformation.transform(this.outstanding, operation);
          this.outstanding = pair1[0];
          var operationPrime = pair1[1];
          var pair2 = operational_transformation.transform(this.buffer, operationPrime);
          this.buffer = pair2[0];
          this.applyOperation(pair2[1]);
        }
      },
      newRevision: function () {
        return this.serverRevision + 2;
      }
    }
  };

  return {
    Client: Client
  };

})(this);

if (typeof module === 'object') {
  module.exports = ot_client;
}<|endoftext|>


<commit_before>// Dependencies
var KV = require('./kv.js')
var Sender = require('./http_sender.js')
var proto = require('./proto.js')

// Defaults opts
function Options(opts) {
    this.uri = opts.uri || 'http://localhost:8080/'
    this.host = opts.host
    this.port = opts.port
    this.ssl = opts.ssl ||false
    this.user = opts.user || 'root'
    this.http = opts.http || (this.ssl ? require('https') : require('http'))
    this.clock = opts.clock || require('./clock.js')
}

// Client interface fabricator
function NewClient(_opts) {
    if(!(this instanceof NewClient)) {
        return new NewClient(_opts)
    }

    var opts = new Options(_opts)

    this.sender = new Sender(opts)
    this.clock  = opts.clock

    return new Client(this.sender, this.clock)
}

// Client.js represents the exported client interface
// This is the API for our end users
function Client(sender, clock) {
    this.kv = new KV(sender, clock)
    this.Call = this.kv.Call.bind(this.kv) // Default

    return this
}

// Create a new Client
// and Upgrade it to a prepared client.
// This is needed to assure "thread safety"
// Since the kv.Prepare api is expected to be used
// in a sync fashion.
Client.prototype.prepare = function PrepareClient() {
    var client = new Client(this.kv.sender, this.kv.clock)
    client.Call = client.kv.Prepare.bind(client.kv) // Upgrade
    return client
}

// Get key
Client.prototype.get = function Get(key, callback) {
    this.Call(proto.Get, proto.GetArgs(key), function(err, res) {
        // Extract value from res
        if(!err && res) {
            if(res.value.bytes) {
                var value = res.value.bytes.buffer || null
            }
        }
        callback(err, value, res)
    })
}

// Put key
Client.prototype.put = function Put(key, value, callback) {
    this.Call(proto.Put, proto.PutArgs(key, value), callback)
}

// Conditinal Put key
Client.prototype.conditionalPut = function ConditionalPut(key, value, expectedValue, callback) {
    this.Call(proto.ConditionalPut, proto.ConditionalPutArgs(key, value, expectedValue), function(err, res) {
        if(!err) {
            var actualValue = res.actual_value || null
        }
        callback(err, actualValue, res)
    })
}

// Increment
Client.prototype.increment = function Increment(key, value, callback) {
    this.Call(proto.Increment, proto.IncrementArgs(key, value), function(err, res) {
        if(!err) {
            var newValue = res.new_value || null
        }
        callback(err, newValue, res)
    })
}

// Delete
Client.prototype.delete = function Delete(key, callback) {
    this.Call(proto.Delete, proto.DeleteArgs(key), callback)
}

// Delete Range
Client.prototype.deleteRange = function DeleteRange(start_key, end_key, limit, callback) {
    this.Call(proto.DeleteRange, proto.DeleteRangeArgs(start_key, end_key, limit), function(err, res) {
        if(!err) {
            var deleted = res.num_deleted || null
        }
        callback(err, deleted, res)
    })
}

// Scan
Client.prototype.scan = function Scan(key_start, key_end, limit, callback) {
    this.Call(proto.Scan, proto.ScanArgs(key_start, key_end, limit), function(err, res) {
        if(!err) {
            var rows = res.rows || null
        }
        callback(err, rows, res)
    })
}

// Contains
Client.prototype.contains = function Contains(key, callback) {
    this.Call(proto.Contains, proto.ContainsArgs(key), function(err, res) {
        if(!err) {
            var contains = res.contains || null
        }
        callback(err, contains, res)
    })
}

// Flush prepared buffer
Client.prototype.flush = function Flush(callback) {
    this.kv.Flush(callback)
}

// Request to for a transactional client
Client.prototype.runTransaction = function RunTransaction(opts, txn, callback) {
    var client = new Client(this.kv.sender, this.kv.clock)
    client.kv.RunTransaction(opts, function TxnFnc(_kv, commit, abort){
        // Upgrade the standard kv client to our client
        txn(client, commit, abort)
    }, callback)
    return
}

// Exports
module.exports = NewClient
<commit_msg>Fix client so TxnSender is actually used.<commit_after>// Dependencies
var KV = require('./kv.js')
var Sender = require('./http_sender.js')
var proto = require('./proto.js')

// Defaults opts
function Options(opts) {
    this.uri = opts.uri || 'http://localhost:8080/'
    this.host = opts.host
    this.port = opts.port
    this.ssl = opts.ssl ||false
    this.user = opts.user || 'root'
    this.http = opts.http || (this.ssl ? require('https') : require('http'))
    this.clock = opts.clock || require('./clock.js')
}

// Client interface fabricator
function NewClient(_opts) {
    if(!(this instanceof NewClient)) {
        return new NewClient(_opts)
    }

    var opts = new Options(_opts)

    this.sender = new Sender(opts)
    this.clock  = opts.clock

    return new Client(this.sender, this.clock)
}

// Client.js represents the exported client interface
// This is the API for our end users
function Client(sender, clock) {
    this.kv = new KV(sender, clock)
    this.Call = this.kv.Call.bind(this.kv) // Default

    return this
}

// Create a new Client
// and Upgrade it to a prepared client.
// This is needed to assure "thread safety"
// Since the kv.Prepare api is expected to be used
// in a sync fashion.
Client.prototype.prepare = function PrepareClient() {
    var client = new Client(this.kv.sender, this.kv.clock)
    client.Call = client.kv.Prepare.bind(client.kv) // Upgrade
    return client
}

// Get key
Client.prototype.get = function Get(key, callback) {
    this.Call(proto.Get, proto.GetArgs(key), function(err, res) {
        // Extract value from res
        if(!err && res) {
            if(res.value.bytes) {
                var value = res.value.bytes.buffer || null
            }
        }
        callback(err, value, res)
    })
}

// Put key
Client.prototype.put = function Put(key, value, callback) {
    this.Call(proto.Put, proto.PutArgs(key, value), callback)
}

// Conditinal Put key
Client.prototype.conditionalPut = function ConditionalPut(key, value, expectedValue, callback) {
    this.Call(proto.ConditionalPut, proto.ConditionalPutArgs(key, value, expectedValue), function(err, res) {
        if(!err) {
            var actualValue = res.actual_value || null
        }
        callback(err, actualValue, res)
    })
}

// Increment
Client.prototype.increment = function Increment(key, value, callback) {
    this.Call(proto.Increment, proto.IncrementArgs(key, value), function(err, res) {
        if(!err) {
            var newValue = res.new_value || null
        }
        callback(err, newValue, res)
    })
}

// Delete
Client.prototype.delete = function Delete(key, callback) {
    this.Call(proto.Delete, proto.DeleteArgs(key), callback)
}

// Delete Range
Client.prototype.deleteRange = function DeleteRange(start_key, end_key, limit, callback) {
    this.Call(proto.DeleteRange, proto.DeleteRangeArgs(start_key, end_key, limit), function(err, res) {
        if(!err) {
            var deleted = res.num_deleted || null
        }
        callback(err, deleted, res)
    })
}

// Scan
Client.prototype.scan = function Scan(key_start, key_end, limit, callback) {
    this.Call(proto.Scan, proto.ScanArgs(key_start, key_end, limit), function(err, res) {
        if(!err) {
            var rows = res.rows || null
        }
        callback(err, rows, res)
    })
}

// Contains
Client.prototype.contains = function Contains(key, callback) {
    this.Call(proto.Contains, proto.ContainsArgs(key), function(err, res) {
        if(!err) {
            var contains = res.contains || null
        }
        callback(err, contains, res)
    })
}

// Flush prepared buffer
Client.prototype.flush = function Flush(callback) {
    this.kv.Flush(callback)
}

// Request to for a transactional client
Client.prototype.runTransaction = function RunTransaction(opts, txn, callback) {
    this.kv.RunTransaction(opts, function TxnFnc(_kv, commit, abort){
        var client = new Client(_kv.sender, _kv.clock)
        txn(client, commit, abort)
    }, callback)
}

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


<commit_before>/*!
 * @license MIT
 * Client side js to use wechat-jssdk, also works with other server side service.
 * https://github.com/JasonBoy/wechat-jssdk
 */

'use strict';

const document = window.document;
const location = window.location;

//default wechat script url
let defaultScriptUrl =
  location.protocol + '//res.wx.qq.com/open/js/jweixin-1.3.2.js';

//default apis with share-on-moment and share-on-chat
const defaultApiList = ['onMenuShareTimeline', 'onMenuShareAppMessage'];

class WechatJSSDK {
  /**
   * Initialize the WechatJSSDK instance
   * @constructor
   * @param {object} wechatConfig, should contain like:
   *   {
   *      appId: 'xxxx',
   *      timestamp: '',
   *      nonceStr: '',
   *      signature: '',
   *      jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', ...],
   *      success: function(){}, //sign success callback
   *      error: function(){}, //sign error callback
   *      customUrl: 'http://res.wx.qq.com/open/js/jweixin-1.0.0.js' // set custom weixin script url
   *   }
   * @returns {WechatJSSDK}
   */
  constructor(wechatConfig) {
    //using new WechatJSSDK(config);
    if (this instanceof WechatJSSDK) {
      this.config = wechatConfig || {};
      if (this.config.customUrl) {
        defaultScriptUrl = this.config.customUrl;
        delete this.config.customUrl;
      }
      let apiList = this.config.jsApiList;
      //add more apis if passed in
      if (!apiList || apiList.length <= 0) {
        this.config.jsApiList = defaultApiList;
      } else {
        let i = 0;
        const length = defaultApiList.length;
        for (; i < length; i++) {
          const defaultItem = defaultApiList[i];
          if (apiList.indexOf(defaultItem) < 0) {
            apiList.push(defaultItem);
          }
        }
      }
      this.debug = !!this.config.debug;
      this.loadScript();
      return this;
    }
    return new WechatJSSDK(wechatConfig);
  }

  /**
   * Sign the signature now
   * @param {object} [newSignConfig], debug mode, appId, jsApiList cannot be changed!!!
   *        , should only provide new signature specific config
   * @returns {WechatJSSDK} sdk instance
   */
  signSignature(newSignConfig) {
    const selfConfig = this.config;
    const config = newSignConfig || selfConfig;
    const signConfig = {
      debug: this.debug,
      appId: selfConfig.appId,
      timestamp: config.timestamp || selfConfig.timestamp,
      nonceStr: config.nonceStr || selfConfig.nonceStr,
      signature: config.signature || selfConfig.signature,
      jsApiList: selfConfig.jsApiList.slice(0, selfConfig.jsApiList.length),
    };
    const debug = this.debug;
    if (!window.wx) {
      console.warn('wechat js not defined');
      return this;
    }
    const wx = window.wx;
    //export original wx object
    this.setOriginWx();
    wx.config(signConfig);
    wx.ready(() => {
      console.log('sign signature finished...');
      this.setOriginWx();
      // debug && alert('sign signature finished...');
      config.success && config.success.call(this, this);
    });
    wx.error(err => {
      debug && alert('sign error: ' + JSON.stringify(err));
      this.setOriginWx();
      config.error && config.error.call(this, err, this);
    });

    return this;
  }

  /**
   * Load wechat js script and sign the signature
   * @returns {WechatJSSDK}
   */
  loadScript() {
    const ele = document.createElement('script');
    ele.type = 'text/javascript';
    ele.async = true;
    ele.onload = () => {
      console.log('Wechat script loaded successfully!');
      //init the wechat config
      this.signSignature();
    };
    ele.onerror = err => {
      console.error('Failed to load wechat script!');
      console.error(err);
      this.debug && alert('Cannot load wechat script!');
    };
    const linkEle = document.getElementsByTagName('script')[0];
    linkEle.parentNode.insertBefore(ele, linkEle);
    ele.src = defaultScriptUrl;
    return this;
  }

  /**
   * Quick way to set custom moment share configs
   * @param {object} info
   * @returns {WechatJSSDK}
   */
  shareOnMoment(info) {
    if (!info) return this;
    return this.callWechatApi('onMenuShareTimeline', info);
  }

  /**
   * Quick way to set custom chat share configs
   * @param {object} info
   * @returns {WechatJSSDK}
   */
  shareOnChat(info) {
    if (!info) return this;
    return this.callWechatApi('onMenuShareAppMessage', info);
  }

  /**
   * Call any wechat api
   * @param {string} apiName
   * @param {object} config specific api config
   * @returns {WechatJSSDK}
   */
  callWechatApi(apiName, config) {
    if (!apiName) return this;
    const debug = this.debug;
    if (this.config.jsApiList.indexOf(apiName) < 0) {
      debug &&
        alert(
          'the wechat api [' +
            apiName +
            '] you call was not registered, \npls add the api into your [jsApiList] config'
        );
      return this;
    }
    const wx = this.getOriginalWx();
    let customAPI = wx[apiName];
    if (!customAPI || 'function' !== typeof customAPI) {
      debug && alert('no such api [' + apiName + '] found!');
      return this;
    }
    customAPI(config);
    return this;
  }

  /**
   * get the original wx object directly
   * @return {*}
   */
  getOriginalWx() {
    return this.wx || window.wx;
  }

  /**
   * check and set the original wx to this
   * @returns {WechatJSSDK}
   */
  setOriginWx() {
    if (!this.wx) {
      this.wx = window.wx;
    }
    return this;
  }
}

module.exports = WechatJSSDK;
<commit_msg>:art:Use export default for client js<commit_after>/*!
 * @license MIT
 * Client side js to use wechat-jssdk, also works with other server side service.
 * https://github.com/JasonBoy/wechat-jssdk
 */

'use strict';

const document = window.document;
const location = window.location;

//default wechat script url
let defaultScriptUrl =
  location.protocol + '//res.wx.qq.com/open/js/jweixin-1.3.2.js';

//default apis with share-on-moment and share-on-chat
const defaultApiList = ['onMenuShareTimeline', 'onMenuShareAppMessage'];

class WechatJSSDK {
  /**
   * Initialize the WechatJSSDK instance
   * @constructor
   * @param {object} wechatConfig, should contain like:
   *   {
   *      appId: 'xxxx',
   *      timestamp: '',
   *      nonceStr: '',
   *      signature: '',
   *      jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', ...],
   *      success: function(){}, //sign success callback
   *      error: function(){}, //sign error callback
   *      customUrl: 'http://res.wx.qq.com/open/js/jweixin-1.0.0.js' // set custom weixin script url
   *   }
   * @returns {WechatJSSDK}
   */
  constructor(wechatConfig) {
    //using new WechatJSSDK(config);
    if (this instanceof WechatJSSDK) {
      this.config = wechatConfig || {};
      if (this.config.customUrl) {
        defaultScriptUrl = this.config.customUrl;
        delete this.config.customUrl;
      }
      let apiList = this.config.jsApiList;
      //add more apis if passed in
      if (!apiList || apiList.length <= 0) {
        this.config.jsApiList = defaultApiList;
      } else {
        let i = 0;
        const length = defaultApiList.length;
        for (; i < length; i++) {
          const defaultItem = defaultApiList[i];
          if (apiList.indexOf(defaultItem) < 0) {
            apiList.push(defaultItem);
          }
        }
      }
      this.debug = !!this.config.debug;
      this.loadScript();
      return this;
    }
    return new WechatJSSDK(wechatConfig);
  }

  /**
   * Sign the signature now
   * @param {object} [newSignConfig], debug mode, appId, jsApiList cannot be changed!!!
   *        , should only provide new signature specific config
   * @returns {WechatJSSDK} sdk instance
   */
  signSignature(newSignConfig) {
    const selfConfig = this.config;
    const config = newSignConfig || selfConfig;
    const signConfig = {
      debug: this.debug,
      appId: selfConfig.appId,
      timestamp: config.timestamp || selfConfig.timestamp,
      nonceStr: config.nonceStr || selfConfig.nonceStr,
      signature: config.signature || selfConfig.signature,
      jsApiList: selfConfig.jsApiList.slice(0, selfConfig.jsApiList.length),
    };
    const debug = this.debug;
    if (!window.wx) {
      console.warn('wechat js not defined');
      return this;
    }
    const wx = window.wx;
    //export original wx object
    this.setOriginWx();
    wx.config(signConfig);
    wx.ready(() => {
      console.log('sign signature finished...');
      this.setOriginWx();
      // debug && alert('sign signature finished...');
      config.success && config.success.call(this, this);
    });
    wx.error(err => {
      debug && alert('sign error: ' + JSON.stringify(err));
      this.setOriginWx();
      config.error && config.error.call(this, err, this);
    });

    return this;
  }

  /**
   * Load wechat js script and sign the signature
   * @returns {WechatJSSDK}
   */
  loadScript() {
    const ele = document.createElement('script');
    ele.type = 'text/javascript';
    ele.async = true;
    ele.onload = () => {
      console.log('Wechat script loaded successfully!');
      //init the wechat config
      this.signSignature();
    };
    ele.onerror = err => {
      console.error('Failed to load wechat script!');
      console.error(err);
      this.debug && alert('Cannot load wechat script!');
    };
    const linkEle = document.getElementsByTagName('script')[0];
    linkEle.parentNode.insertBefore(ele, linkEle);
    ele.src = defaultScriptUrl;
    return this;
  }

  /**
   * Quick way to set custom moment share configs
   * @param {object} info
   * @returns {WechatJSSDK}
   */
  shareOnMoment(info) {
    if (!info) return this;
    return this.callWechatApi('onMenuShareTimeline', info);
  }

  /**
   * Quick way to set custom chat share configs
   * @param {object} info
   * @returns {WechatJSSDK}
   */
  shareOnChat(info) {
    if (!info) return this;
    return this.callWechatApi('onMenuShareAppMessage', info);
  }

  /**
   * Call any wechat api
   * @param {string} apiName
   * @param {object} config specific api config
   * @returns {WechatJSSDK}
   */
  callWechatApi(apiName, config) {
    if (!apiName) return this;
    const debug = this.debug;
    if (this.config.jsApiList.indexOf(apiName) < 0) {
      debug &&
        alert(
          'the wechat api [' +
            apiName +
            '] you call was not registered, \npls add the api into your [jsApiList] config'
        );
      return this;
    }
    const wx = this.getOriginalWx();
    let customAPI = wx[apiName];
    if (!customAPI || 'function' !== typeof customAPI) {
      debug && alert('no such api [' + apiName + '] found!');
      return this;
    }
    customAPI(config);
    return this;
  }

  /**
   * get the original wx object directly
   * @return {*}
   */
  getOriginalWx() {
    return this.wx || window.wx;
  }

  /**
   * check and set the original wx to this
   * @returns {WechatJSSDK}
   */
  setOriginWx() {
    if (!this.wx) {
      this.wx = window.wx;
    }
    return this;
  }
}

export default WechatJSSDK;
<|endoftext|>