diff --git a/packages/xo-lib/.babelrc b/packages/xo-lib/.babelrc new file mode 100644 index 000000000..5e4035751 --- /dev/null +++ b/packages/xo-lib/.babelrc @@ -0,0 +1,8 @@ +{ + "comments": false, + "compact": true, + "presets": [ + "stage-0", + "es2015" + ] +} diff --git a/packages/xo-lib/.editorconfig b/packages/xo-lib/.editorconfig index e35119a31..da21ef4c5 100644 --- a/packages/xo-lib/.editorconfig +++ b/packages/xo-lib/.editorconfig @@ -1,12 +1,65 @@ -# EditorConfig is awesome: http://EditorConfig.org +# http://EditorConfig.org +# +# Julien Fontanet's configuration +# https://gist.github.com/julien-f/8096213 -# top-most EditorConfig file +# Top-most EditorConfig file. root = true +# Common config. [*] charset = utf-8 end_of_line = lf -indent_size = 2 -indent_style = space insert_final_newline = true trim_trailing_whitespaces = true + +# CoffeeScript +# +# https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md +[*.{,lit}coffee] +indent_size = 2 +indent_style = space + +# Markdown +[*.{md,mdwn,mdown,markdown}] +indent_size = 4 +indent_style = space + +# Package.json +# +# This indentation style is the one used by npm. +[/package.json] +indent_size = 2 +indent_style = space + +# Jade +[*.jade] +indent_size = 2 +indent_style = space + +# JavaScript +# +# Two spaces seems to be the standard most common style, at least in +# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces). +[*.js] +indent_size = 2 +indent_style = space + +# Less +[*.less] +indent_size = 2 +indent_style = space + +# Sass +# +# Style used for http://libsass.com +[*.s[ac]ss] +indent_size = 2 +indent_style = space + +# YAML +# +# Only spaces are allowed. +[*.yaml] +indent_size = 2 +indent_style = space diff --git a/packages/xo-lib/.gitignore b/packages/xo-lib/.gitignore index 2ccbe4656..6959be1cf 100644 --- a/packages/xo-lib/.gitignore +++ b/packages/xo-lib/.gitignore @@ -1 +1,9 @@ -/node_modules/ +/.nyc_output/ +/bower_components/ +/dist/ + +npm-debug.log +npm-debug.log.* + +!node_modules/* +node_modules/*/ diff --git a/packages/xo-lib/.mocha.js b/packages/xo-lib/.mocha.js new file mode 100644 index 000000000..e6d84e403 --- /dev/null +++ b/packages/xo-lib/.mocha.js @@ -0,0 +1,5 @@ +Error.stackTraceLimit = 100 + +try { require('trace') } catch (_) {} +try { require('clarify') } catch (_) {} +try { require('source-map-support/register') } catch (_) {} diff --git a/packages/xo-lib/.mocha.opts b/packages/xo-lib/.mocha.opts new file mode 100644 index 000000000..6cfd94898 --- /dev/null +++ b/packages/xo-lib/.mocha.opts @@ -0,0 +1 @@ +--require ./.mocha.js diff --git a/packages/xo-lib/.npmignore b/packages/xo-lib/.npmignore index 889f827ca..c31ee82cb 100644 --- a/packages/xo-lib/.npmignore +++ b/packages/xo-lib/.npmignore @@ -1 +1,10 @@ +/examples/ +example.js +example.js.map +*.example.js +*.example.js.map + +/test/ +/tests/ *.spec.js +*.spec.js.map diff --git a/packages/xo-lib/.travis.yml b/packages/xo-lib/.travis.yml index 427dd41f8..502095fce 100644 --- a/packages/xo-lib/.travis.yml +++ b/packages/xo-lib/.travis.yml @@ -1,5 +1,10 @@ language: node_js node_js: + - 'stable' + - '4' + - '0.12' - '0.10' - - '0.11' - - iojs + +# Use containers. +# http://docs.travis-ci.com/user/workers/container-based-infrastructure/ +sudo: false diff --git a/packages/xo-lib/api.js b/packages/xo-lib/api.js deleted file mode 100644 index 351167260..000000000 --- a/packages/xo-lib/api.js +++ /dev/null @@ -1,151 +0,0 @@ -'use strict' - -// =================================================================== - -var Bluebird = require('bluebird') -var EventEmitter = require('events').EventEmitter -var eventToPromise = require('event-to-promise') -var inherits = require('util').inherits -var MethodNotFound = require('json-rpc-peer').MethodNotFound -var Peer = require('json-rpc-peer').default -var startsWith = require('lodash.startswith') -var WebSocket = require('ws') - -var ConnectionError = require('./connection-error') -var fixUrl = require('./fix-url') - -// =================================================================== - -function getCurrentUrl () { - /* global window: false */ - - if (typeof window === 'undefined') { - throw new Error('cannot get current URL') - } - - return String(window.location) -} - -function makeDeferred () { - var resolve, reject - var promise = new Bluebird(function (resolve_, reject_) { - resolve = resolve_ - reject = reject_ - }) - - return { - promise: promise, - reject: reject, - resolve: resolve - } -} - -function noop () {} - -// ------------------------------------------------------------------- - -// Low level interface to XO. -function Api (url) { - // Super constructor. - EventEmitter.call(this) - - // Fix the URL (ensure correct protocol and /api/ path). - this._url = fixUrl(url || getCurrentUrl()) - - // Will contains the connection promise. - this._connection = null - - // Will contains the WebSocket. - this._socket = null - - // The JSON-RPC server. - var this_ = this - this._jsonRpc = new Peer(function (message) { - if (message.type === 'notification') { - this_.emit('notification', message) - } else { - // This object does not support requests. - throw new MethodNotFound(message.method) - } - }).on('data', function (message) { - this_._socket.send(message) - }) -} -inherits(Api, EventEmitter) - -Api.prototype.close = function () { - var socket = this._socket - if (socket) { - socket.close() - - console.log(socket.readyState) - if (socket.readyState !== 3) { - return eventToPromise(socket, 'close').then(noop) - } - } - - return Bluebird.resolve() -} - -Api.prototype.connect = function () { - if (this._connection) { - return this._connection - } - - var deferred = makeDeferred() - this._connection = deferred.promise - - var opts = {} - if (startsWith(this._url, 'wss')) { - // Due to imperfect TLS implementation in XO-Server. - opts.rejectUnauthorized = false - } - var socket = this._socket = new WebSocket(this._url, '', opts) - - // Used to avoid binding listeners to this object. - var this_ = this - - // When the socket opens, send any queued requests. - socket.addEventListener('open', function () { - // Resolves the promise. - deferred.resolve() - - this_.emit('connected') - }) - - socket.addEventListener('error', function (error) { - this_._connection = null - this_._socket = null - - // Fails the connect promise if possible. - deferred.reject(error) - }) - - socket.addEventListener('message', function (message) { - this_._jsonRpc.write(message.data) - }) - - socket.addEventListener('close', function () { - this_._connection = null - this_._socket = null - - this_._jsonRpc.failPendingRequests(new ConnectionError()) - - // Only emit this event if connected before. - if (deferred.promise.isFulfilled()) { - this_.emit('disconnected') - } - }) - - return deferred.promise -} - -Api.prototype.call = function (method, params) { - var jsonRpc = this._jsonRpc - - return this.connect().then(function () { - return jsonRpc.request(method, params) - }) -} - -module.exports = Api diff --git a/packages/xo-lib/back-off.js b/packages/xo-lib/back-off.js deleted file mode 100644 index d79610c31..000000000 --- a/packages/xo-lib/back-off.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict' - -// =================================================================== - -var Bluebird = require('bluebird') - -// =================================================================== - -function returnThis () { - /* jshint validthis: true */ - - return this -} - -// Returns an iterator to the Fibonacci sequence. -function fibonacci (start) { - var prev = 0 - var curr = start || 1 - - var iterator = { - next: function () { - var tmp = curr - curr += prev - prev = tmp - - return { - done: false, - value: prev - } - } - } - - // Make the iterator a true iterable (ES6). - if (typeof Symbol !== 'undefined') { - iterator[Symbol.iterator] = returnThis - } - - return iterator -} - -// =================================================================== - -function defaultGenerator () { - return fibonacci(1e3) -} - -function BackOff (opts) { - if (!opts) { - opts = {} - } - - this._attempts = 0 - this._generator = opts.generator || defaultGenerator - this._iterator = this._generator() - this._maxAttempts = opts.maxAttempts || Infinity -} - -BackOff.prototype.wait = function () { - var maxAttempts = this._maxAttempts - if (this._attempts++ > maxAttempts) { - return Bluebird.reject(new Error( - 'maximum attempts reached (' + maxAttempts + ')' - )) - } - - return Bluebird.delay(this._iterator.next().value) -} - -BackOff.prototype.reset = function () { - this._attempts = 0 - this._iterator = this._generator() -} - -// =================================================================== - -module.exports = BackOff diff --git a/packages/xo-lib/cli.js b/packages/xo-lib/cli.js deleted file mode 100755 index 9c1af26fd..000000000 --- a/packages/xo-lib/cli.js +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env node - -'use strict' - -var Bluebird = require('bluebird') -var createRepl = require('repl').start -var eventToPromise = require('event-to-promise') -var pw = require('pw') - -var Xo = require('./').Xo - -// =================================================================== - -var usage = '' - -function main (args) { - if (args[0] === '--help' || args[0] === 'h') { - return usage - } - - var xo = new Xo(args[0]) - - return new Bluebird(function (resolve) { - process.stdout.write('Password: ') - pw(resolve) - }).then(function (password) { - return xo.signIn({ - email: args[1], - password: password - }) - }).then(function () { - var repl = createRepl({}) - repl.context.xo = xo - - // Make the REPL waits for promise completion. - var evaluate = Bluebird.promisify(repl.eval) - repl.eval = function (cmd, context, filename, cb) { - evaluate(cmd, context, filename) - // See https://github.com/petkaantonov/bluebird/issues/594 - .then(function (result) { return result }) - .nodeify(cb) - } - - return eventToPromise(repl, 'exit') - }) -} -module.exports = main - -// =================================================================== - -if (!module.parent) { - require('exec-promise')(main) -} diff --git a/packages/xo-lib/connection-error.js b/packages/xo-lib/connection-error.js deleted file mode 100644 index a0f6ab1a0..000000000 --- a/packages/xo-lib/connection-error.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -// =================================================================== - -var makeError = require('make-error') - -// =================================================================== - -module.exports = makeError('ConnectionError') diff --git a/packages/xo-lib/fix-url.js b/packages/xo-lib/fix-url.js deleted file mode 100644 index d767d5589..000000000 --- a/packages/xo-lib/fix-url.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -// =================================================================== - -// Fix URL if necessary. -var URL_RE = /^(?:(?:http|ws)(s)?:\/\/)?(.*?)\/*(?:\/api\/)?(\?.*?)?(?:#.*)?$/ -function fixUrl (url) { - var matches = URL_RE.exec(url) - var isSecure = !!matches[1] - var hostAndPath = matches[2] - var search = matches[3] - - return [ - isSecure ? 'wss' : 'ws', - '://', - hostAndPath, - '/api/', - search - ].join('') -} -module.exports = fixUrl diff --git a/packages/xo-lib/fix-url.spec.js b/packages/xo-lib/fix-url.spec.js deleted file mode 100644 index 2ff751050..000000000 --- a/packages/xo-lib/fix-url.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict' - -// =================================================================== - -var expect = require('must') - -// =================================================================== - -/* eslint-env mocha */ - -describe('fixUrl()', function () { - var fixUrl = require('./fix-url') - - describe('protocol', function () { - it('is added if missing', function () { - expect(fixUrl('localhost/api/')).to.equal('ws://localhost/api/') - }) - - it('HTTP(s) is converted to WS(s)', function () { - expect(fixUrl('http://localhost/api/')).to.equal('ws://localhost/api/') - expect(fixUrl('https://localhost/api/')).to.equal('wss://localhost/api/') - }) - - it('is not added if already present', function () { - expect(fixUrl('ws://localhost/api/')).to.equal('ws://localhost/api/') - expect(fixUrl('wss://localhost/api/')).to.equal('wss://localhost/api/') - }) - }) - - describe('/api/ path', function () { - it('is added if missing', function () { - expect(fixUrl('ws://localhost')).to.equal('ws://localhost/api/') - expect(fixUrl('ws://localhost/')).to.equal('ws://localhost/api/') - }) - - it('is not added if already present', function () { - expect(fixUrl('ws://localhost/api/')).to.equal('ws://localhost/api/') - }) - - it('removes the hash part', function () { - expect(fixUrl('ws://localhost/#foo')).to.equal('ws://localhost/api/') - }) - - it('conserve the search part', function () { - expect(fixUrl('ws://localhost/?foo')).to.equal('ws://localhost/api/?foo') - }) - }) -}) diff --git a/packages/xo-lib/index.js b/packages/xo-lib/index.js deleted file mode 100644 index 695713bfd..000000000 --- a/packages/xo-lib/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -// Expose Bluebird for now to ease integration (e.g. with Angular.js). -exports.setScheduler = require('bluebird').setScheduler - -exports.Api = require('./api') -exports.Xo = require('./xo') diff --git a/packages/xo-lib/package.json b/packages/xo-lib/package.json index 090b36890..06168451e 100644 --- a/packages/xo-lib/package.json +++ b/packages/xo-lib/package.json @@ -1,12 +1,15 @@ { "name": "xo-lib", - "version": "0.7.4", + "version": "0.8.0-1", + "license": "ISC", "description": "Library to connect to XO-Server", "keywords": [ "xen", "orchestra", "xen-orchestra" ], + "homepage": "https://github.com/vatesfr/xo-lib", + "bugs": "https://github.com/vatesfr/xo-lib/issues", "repository": { "type": "git", "url": "https://github.com/vatesfr/xo-lib" @@ -15,31 +18,48 @@ "name": "Julien Fontanet", "email": "julien.fontanet@vates.fr" }, + "preferGlobal": false, + "main": "dist/", + "bin": {}, + "files": [ + "dist/" + ], "engines": { - "node": ">=0.8.0" - }, - "scripts": { - "lint": "standard", - "posttest": "npm run lint", - "test": "mocha \"*.spec.js\"" + "node": ">=0.12" }, "dependencies": { - "bluebird": "^2.9.6", - "event-to-promise": "^0.4.0", - "exec-promise": "^0.5.1", - "json-rpc-peer": "^0.11.0", - "lodash.assign": "^3.0.0", - "lodash.foreach": "^3.0.1", - "lodash.isstring": "^3.0.0", - "lodash.startswith": "^3.0.0", - "make-error": "^1.0.4", - "pw": "0.0.4", - "ws": "^0.8.0", - "xo-collection": "^0.4.0" + "jsonrpc-websocket-client": "0.0.1-5", + "lodash.startswith": "^4.0.0", + "make-error": "^1.0.4" }, "devDependencies": { - "mocha": "^2.1.0", + "babel-cli": "^6.3.17", + "babel-eslint": "^6.0.4", + "babel-preset-es2015": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", + "clarify": "^1.0.5", + "dependency-check": "^2.5.1", + "mocha": "^2.3.4", "must": "^0.13.1", - "standard": "^5.3.1" + "nyc": "^6.1.1", + "source-map-support": "^0.4.0", + "standard": "^6.0.8", + "trace": "^2.0.1" + }, + "scripts": { + "build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/", + "depcheck": "dependency-check ./package.json", + "dev": "babel --watch --source-maps --out-dir=dist/ src/", + "dev-test": "mocha --opts .mocha.opts --watch --reporter=min \"dist/**/*.spec.js\"", + "lint": "standard", + "posttest": "npm run lint && npm run depcheck", + "prepublish": "npm run build", + "test": "nyc mocha --opts .mocha.opts \"dist/**/*.spec.js\"" + }, + "standard": { + "ignore": [ + "dist/**" + ], + "parser": "babel-eslint" } } diff --git a/packages/xo-lib/session-error.js b/packages/xo-lib/session-error.js deleted file mode 100644 index 19c54f400..000000000 --- a/packages/xo-lib/session-error.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -// =================================================================== - -var makeError = require('make-error') - -// =================================================================== - -module.exports = makeError('SessionError') diff --git a/packages/xo-lib/src/index.js b/packages/xo-lib/src/index.js new file mode 100644 index 000000000..67aee4ce1 --- /dev/null +++ b/packages/xo-lib/src/index.js @@ -0,0 +1,71 @@ +import startsWith from 'lodash.startswith' +import JsonRpcWebSocketClient, { + OPEN, + CLOSED +} from 'jsonrpc-websocket-client' +import { BaseError } from 'make-error' + +// =================================================================== + +const noop = () => {} + +// =================================================================== + +export class XoError extends BaseError {} + +// ------------------------------------------------------------------- + +export default class Xo extends JsonRpcWebSocketClient { + constructor (opts) { + const url = opts && opts.url || '.' + super(`${url === '/' ? '' : url}/api/`) + + this._credentials = opts && opts.credentials || null + this._user = null + + this.on(OPEN, () => { + if (this._credentials) { + this._signIn(this._credentials).catch(noop) + } + }) + this.on(CLOSED, () => { + this._user = null + }) + } + + get user () { + return this._user + } + + call (method, args, i) { + if (startsWith(method, 'session.')) { + return Promise.reject( + new XoError('session.*() methods are disabled from this interface') + ) + } + + const promise = super.call(method, args) + promise.retry = (predicate) => promise.catch((error) => { + i = (i || 0) + 1 + if (predicate(error, i)) { + return this.call(method, args, i) + } + }) + + return promise + } + + signIn (credentials) { + // Register this credentials for future use. + this._credentials = credentials + + return this._signIn(credentials) + } + + _signIn (credentials) { + return super.call('session.signIn', credentials).then((user) => { + this._user = user + this.emit('authenticated') + }) + } +} diff --git a/packages/xo-lib/src/index.spec.js b/packages/xo-lib/src/index.spec.js new file mode 100644 index 000000000..2319bd7d5 --- /dev/null +++ b/packages/xo-lib/src/index.spec.js @@ -0,0 +1,17 @@ +/* eslint-env mocha */ + +import expect from 'must' + +// =================================================================== + +import myLib from './' + +// =================================================================== + +describe.skip('myLib', () => { + it('does something', () => { + // TODO: some real tests. + + expect(myLib).to.exists() + }) +}) diff --git a/packages/xo-lib/xo.js b/packages/xo-lib/xo.js deleted file mode 100644 index 336865d77..000000000 --- a/packages/xo-lib/xo.js +++ /dev/null @@ -1,282 +0,0 @@ -'use strict' - -// =================================================================== - -var Bluebird = require('bluebird') -var Collection = require('xo-collection').default -var forEach = require('lodash.foreach') -var Index = require('xo-collection/index') -var isString = require('lodash.isstring') -var startsWith = require('lodash.startswith') -var UniqueIndex = require('xo-collection/unique-index') - -var Api = require('./api') -var BackOff = require('./back-off') -var ConnectionError = require('./connection-error') -var SessionError = require('./session-error') - -// =================================================================== - -// function bind (fn, thisArg) { -// if (!fn) { -// return fn -// } - -// return function () { -// return fn.apply(thisArg, arguments) -// } -// } - -function makeStandaloneDeferred () { - var resolve, reject - - var promise = new Bluebird(function (resolve_, reject_) { - resolve = resolve_ - reject = reject_ - }) - promise.resolve = resolve - promise.reject = reject - - return promise -} - -function noop () {} - -// var trace = -// bind(console.trace, console) || -// bind(console.log, console) || -// noop - -// ------------------------------------------------------------------- - -var defineProperty = Object.defineProperty - -function getDeprecatedUUID () { - // trace('.UUID is deprecated, use .id instead') - - return this.id -} - -function defineDeprecatedUUID (object) { - defineProperty(object, 'UUID', { - get: getDeprecatedUUID - }) -} - -// var LINK_RE = /^(.*)\$link\$$/ -// function createAutoLinks (collection, object) { -// var all = collection.all - -// forEach(object, function resolveObject (value, key, object) { -// var matches = key.match(LINK_RE) -// if (!matches) { -// return -// } - -// defineProperty(object, matches[1], { -// get: function () { -// return all[value] -// } -// }) -// }) -// } - -function setMultiple (collection, items) { - var messages = collection.indexes.messagesByObject - - forEach(items, function (item, key) { - defineDeprecatedUUID(item) - // createAutoLinks(collection, item) - - defineProperty(item, 'messages', { - get: function () { - return messages[key] - } - }) - - collection.set(key, item) - }) -} - -function unsetMultiple (collection, items) { - forEach(items, function (_, key) { - if (collection.has(key)) { - collection.remove(key) - } - }) -} - -// =================================================================== - -function Xo (opts) { - if (!opts) { - opts = {} - } else if (isString(opts)) { - opts = { - url: opts - } - } - - // ----------------------------------------------------------------- - - var api = new Api(opts.url) - - api.on('connected', function () { - this._backOff.reset() - this.status = 'connected' - - this._tryToOpenSession() - }.bind(this)) - - api.on('disconnected', function () { - this._closeSession() - this._connect() - }.bind(this)) - - api.on('notification', function (notification) { - if (notification.method !== 'all') { - return - } - - var method = notification.params.type === 'exit' - ? unsetMultiple - : setMultiple - - method(this.objects, notification.params.items) - }.bind(this)) - - // ----------------------------------------------------------------- - - var objects = this.objects = new Collection() - objects.createIndex('ref', new UniqueIndex('ref')) - objects.createIndex('type', new Index('type')) - objects.createIndex('messagesByObject', new Index(function (obj) { - if (obj.type === 'message') { - return obj.$object - } - })) - - this.status = 'disconnected' - this.user = null - - this._api = api - this._backOff = new BackOff() - this._credentials = opts.creadentials - this._session = makeStandaloneDeferred() - this._signIn = null - - // ----------------------------------------------------------------- - - this._connect() -} - -Xo.prototype.call = function (method, params, retryOnConnectionError) { - // Prevent session.*() from being because it may interfere - // with this class session management. - if (startsWith(method, 'session.')) { - return Bluebird.reject( - new Error('session.*() methods are disabled from this interface') - ) - } - - var this_ = this - return this._session.then(function () { - return this_._api.call(method, params) - }).catch(function (error) { - if ( - error instanceof SessionError || - retryOnConnectionError && error instanceof ConnectionError - ) { - // Automatically requeue this call. - return this_.call(method, params) - } - - throw error - }) -} - -Xo.prototype.signIn = function (credentials) { - this.signOut() - - this._credentials = credentials - this._signIn = makeStandaloneDeferred() - - this._tryToOpenSession() - - return this._signIn -} - -Xo.prototype.signOut = function () { - this._closeSession() - this._credentials = null - - var signIn = this._signIn - if (signIn && signIn.isPending()) { - signIn.reject(new SessionError('sign in aborted')) - } - - return this.status === 'connected' - - // Attempt to sign out and ignore any return values and errors. - ? this._api.call('session.signOut').then(noop, noop) - - // Always return a promise. - : Bluebird.resolve() -} - -Xo.prototype._connect = function _connect () { - this.status = 'connecting' - - return this._api.connect().bind(this).catch(function (error) { - console.warn('could not connect:', error) - - return this._backOff.wait().bind(this).then(_connect) - }) -} - -Xo.prototype._closeSession = function () { - if (!this._session.isPending()) { - this._session = makeStandaloneDeferred() - } - - this.user = null -} - -Xo.prototype._tryToOpenSession = function () { - var credentials = this._credentials - if (!credentials || this.status !== 'connected') { - return - } - - this._api.call('session.signIn', credentials).bind(this).then( - function (user) { - this.user = user - - this._api.call('xo.getAllObjects').bind(this).then(function (objects) { - this.objects.clear() - setMultiple(this.objects, objects) - }) - - // Validate the sign in. - var signIn = this._signIn - if (signIn) { - signIn.resolve() - } - - // Open the session. - this._session.resolve() - }, - - function (error) { - // Reject the sign in. - var signIn = this._signIn - if (signIn) { - signIn.reject(error) - } - } - ) -} - -// =================================================================== - -module.exports = Xo