Compare commits

..

51 Commits

Author SHA1 Message Date
Julien Fontanet
4fd9639457 0.7.3 2015-09-24 16:40:31 +02:00
Julien Fontanet
2f2ee1f431 Update ws 0.8.0 to support Node 4. 2015-09-24 16:39:54 +02:00
Julien Fontanet
725f471a6a 0.7.2 2015-05-28 15:36:49 +02:00
Julien Fontanet
0b01a79d9d Implements object.messages. 2015-05-28 15:36:33 +02:00
Julien Fontanet
2653ff6536 Comment currently unused declarations. 2015-05-28 15:36:04 +02:00
Julien Fontanet
0f30cc8e59 Work around linter issue (eslint/espree#136). 2015-05-28 15:35:27 +02:00
Julien Fontanet
e3879cd4d1 Minor style fix. 2015-05-28 15:34:41 +02:00
Julien Fontanet
7a4cdf8688 Fix connection errors handling in Api#connect(). 2015-05-28 15:33:49 +02:00
Julien Fontanet
1839bf938a 0.7.1 2015-05-27 17:02:25 +02:00
Julien Fontanet
44d4096a79 Remove a console.log(). 2015-05-27 14:07:14 +02:00
Julien Fontanet
41280c9d38 Various updates. 2015-05-27 12:18:41 +02:00
Julien Fontanet
68abd91fc2 Api#close() returns a promise. 2015-05-24 14:41:01 +02:00
Julien Fontanet
5a87a6c502 This lib is not ES6! 2015-05-22 16:41:33 +02:00
Julien Fontanet
d8ca15ceb3 Fix objects removal. 2015-05-22 15:12:22 +02:00
Julien Fontanet
a17f718517 Update xo-collection to 0.3.1. 2015-05-22 14:53:52 +02:00
Julien Fontanet
3589dda8ee Remove ugly hack. 2015-05-22 14:52:27 +02:00
Julien Fontanet
21f8e4d55b Remove unused UUID index. 2015-05-22 14:52:19 +02:00
Julien Fontanet
13913334b6 0.7.0 2015-05-19 16:48:19 +02:00
Julien Fontanet
7f60725c88 Make it work with current version of xo-web. 2015-05-19 16:48:03 +02:00
Julien Fontanet
312fcea5f1 Auto links. 2015-05-15 18:08:21 +02:00
Julien Fontanet
9d05653f5b Use xo-collection. 2015-05-14 17:19:28 +02:00
Julien Fontanet
c763794ef3 Install json-rpc from the npm repository. 2015-04-24 11:21:15 +02:00
Julien Fontanet
99694161e1 The REPL waits for promise completion. 2015-04-21 17:17:00 +02:00
Julien Fontanet
c1db993b92 Fix missing quotes. 2015-04-17 16:25:18 +02:00
Julien Fontanet
c19916ff1c Add CLI. 2015-04-17 14:05:49 +02:00
Julien Fontanet
6fa2e79c1c Use standard code style. 2015-04-17 13:35:33 +02:00
Julien Fontanet
c539dd5570 0.6.3 2015-03-04 18:23:21 +01:00
Julien Fontanet
de76afea99 New implementation of Xo (fixes many issues). 2015-03-04 18:22:11 +01:00
Julien Fontanet
1d3616ae71 Minor changes. 2015-03-04 17:42:22 +01:00
Julien Fontanet
d76cb440f9 Move BackOff to its own module. 2015-03-04 17:42:18 +01:00
Julien Fontanet
7c89d658f7 Fix setScheduler(). 2015-03-04 17:37:12 +01:00
Julien Fontanet
292c929117 Split code in to multiple files. 2015-03-04 17:31:14 +01:00
Julien Fontanet
daf42b63c8 Better back off implementation. 2015-03-04 16:18:57 +01:00
Julien Fontanet
07da03618f Use specific version of json-rpc. 2015-03-04 15:51:35 +01:00
Julien Fontanet
dda51f2801 Xo::call() should never through synchronously. 2015-03-04 15:01:30 +01:00
Julien Fontanet
25472bcfa6 Correctly catch some errors. 2015-03-04 15:01:03 +01:00
Julien Fontanet
6ff17d16f0 0.6.2 2015-02-23 14:48:39 +01:00
Julien Fontanet
06b7116692 Never break object refs for Collection:all & Collection:indexes[*]. 2015-02-23 14:48:32 +01:00
Julien Fontanet
3c3ea0f3e1 Fix Collection::data. 2015-02-23 14:47:11 +01:00
Julien Fontanet
db4d6511d6 0.6.1 2015-02-23 12:25:53 +01:00
Julien Fontanet
6e42a67268 Update README. 2015-02-23 12:25:30 +01:00
Julien Fontanet
fd066e5eef Handle credentials directly in constructor. 2015-02-23 12:25:17 +01:00
Julien Fontanet
3dd0c44410 Handle string parameter in Xo constructor. 2015-02-23 12:24:47 +01:00
Julien Fontanet
12b42854e4 Delete useless example. 2015-02-23 12:23:42 +01:00
Julien Fontanet
2fcb6d0c7c 0.6.0 2015-02-23 12:11:53 +01:00
Julien Fontanet
68e863723a Explicitely handle sign in/out. 2015-02-23 12:10:45 +01:00
Julien Fontanet
d0b37d0f9a Update example. 2015-02-20 17:15:00 +01:00
Julien Fontanet
a0a1353445 Update collection API. 2015-02-20 17:14:44 +01:00
Julien Fontanet
6725cc6f61 0.5.2 2015-02-12 12:23:56 +01:00
Julien Fontanet
7e9639052b getCurrentUrl() must not ignore protocol and search parts. 2015-02-12 12:23:35 +01:00
Julien Fontanet
21bd5ba376 fixUrl() properly handles search and hash parts. 2015-02-12 12:22:58 +01:00
20 changed files with 715 additions and 642 deletions

View File

@@ -1,93 +0,0 @@
{
// Julien Fontanet JSHint configuration
// https://gist.github.com/julien-f/8095615
//
// Changes from defaults:
// - all enforcing options (except `++` & `--`) enabled
// - single quotes
// - indentation set to 2 instead of 4
// - almost all relaxing options disabled
// - environments are set to Node.js
//
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : true, // true: Identifiers must be in camelCase
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"indent" : 2, // {int} Number of spaces to use for indentation
"latedef" : true, // true: Require variables/functions to be defined before being used
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonbsp" : true, // true: Prohibit use of non breakable spaces
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` & `--`
"quotmark" : "single", // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // true: Require all defined variables be used
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
"maxlen" : 80, // {int} Max number of characters per line
"maxparams" : 4, // {int} Max number of formal params allowed per function
"maxstatements" : 20, // {int} Max number statements per function
// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"multistr" : false, // true: Tolerate multi-line strings
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
"noyield" : false, // true: Tolerate generators without yields
// Environments
"browser" : false, // Web Browser (window, document, etc)
"browserify" : false, // Browserify (node.js code in the browser)
"couch" : false, // CouchDB
"devel" : false, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jquery" : false, // jQuery
"mocha" : false, // mocha
"mootools" : false, // MooTools
"node" : true, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"phantom" : false, // PhantomJS
"prototypejs" : false, // Prototype and Scriptaculous
"rhino" : false, // Rhino
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
// Custom Globals
"globals" : {} // additional predefined global variables
}

View File

@@ -0,0 +1 @@
*.spec.js

View File

@@ -28,13 +28,29 @@ This high-level interface handles session sign-in and a cache of
remote XO objects. It also automatically reconnect and retry method
calls when necessary.
```javascript
// Connect to XO.
var xo = new xoLib.Xo('https://xo.company.tld');
// Must sign in before being able to call any methods (all calls will
// be buffered until signed in).
xo.signIn({
email: 'admin@admin.net',
password: 'admin',
}).then(function () {
console('signed as', xo.user);
});
```
The credentials can also be passed directly to the constructor:
```javascript
var xo = new xoLib.Xo({
url: 'https://xo.company.tld',
auth: {
credentials: {
email: 'admin@admin.net',
password: 'admin',
},
}
});
```
@@ -75,13 +91,24 @@ console.log('Current user is', xo.user);
XO objects are cached locally in the `objects` collection.
```javascript
// Get an object for a specific id.
var obj = xo.objects.get(id);
// Read-only dictionary of all objects.
var allObjects = xo.objects.all;
// Get all VMs.
var vms = xo.objects.where('type', 'VM');
// Looks up a given object by its identifier.
var object = allObjects[id];
// Read-only dictionary of all indexes.
var indexes = xo.objects.indexes;
// Read-only dictionary of types.
var byTypes = indexes.type;
// Read-only view of all VMs.
var vms = byTypes.VM;
```
Available indexes are: `ref`, `type` and `UUID`.
## Low level
```javascript

151
packages/xo-lib/api.js Normal file
View File

@@ -0,0 +1,151 @@
'use strict'
// ===================================================================
var Bluebird = require('bluebird')
var EventEmitter = require('events').EventEmitter
var eventToPromise = require('event-to-promise')
var inherits = require('util').inherits
var jsonRpc = require('@julien-f/json-rpc')
var MethodNotFound = require('@julien-f/json-rpc/errors').MethodNotFound
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 = jsonRpc.createServer(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(JSON.stringify(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

View File

@@ -0,0 +1,76 @@
'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

53
packages/xo-lib/cli.js Executable file
View File

@@ -0,0 +1,53 @@
#!/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)
}

View File

@@ -1,153 +0,0 @@
'use strict';
//====================================================================
var forEach = require('lodash.foreach');
var indexOf = require('lodash.indexof');
//====================================================================
function defaultKey(item) {
return item.id || item._id || item;
}
//====================================================================
function Collection(opts) {
if (!opts) {
opts = {};
}
this._key = opts.key || defaultKey;
this._indexes = Object.create(null);
if (opts.indexes) {
forEach(opts.indexes, function (field) {
this[field] = Object.create(null);
}, this._indexes);
}
this._data = Object.create(null);
}
function createIndex(_, field) {
/* jshint validthis: true */
this[field] = Object.create(null);
}
Collection.prototype.clear = function () {
this._data = Object.create(null);
forEach(this._indexes, createIndex, this._indexes);
};
Collection.prototype.get = function (key) {
return this._data[key];
};
// Find the first entry in an index for a given value.
Collection.prototype.find = function (field, value) {
return this.where(field, value)[0];
};
// Find all entries in an index for a given value.
Collection.prototype.where = function (field, value) {
var index = this._indexes[field];
if (!index) {
throw new Error('no such index');
}
return index[value] || [];
};
function unsetItemFromIndex(index, field) {
/* jshint validthis: true */
var prop = this[field];
if (!prop) {
return;
}
var items = index[prop];
var i = indexOf(items, this);
if (i === -1) {
return;
}
// The index contains only this one item for this prop.
if (items.length === 1) {
delete index[prop];
return;
}
// Remove this item.
items.splice(i, 1);
}
// Internal unset method.
function unset(item, key) {
/* jshint validthis: true */
delete this._data[key];
forEach(this._indexes, unsetItemFromIndex, item);
}
function setItemToIndex(index, field) {
/* jshint validthis: true */
var prop = this[field];
if (!prop) {
return;
}
var items = index[prop];
if (items) {
// Update the items list.
items.push(this);
} else {
// Create the items list.
index[prop] = [this];
}
}
Collection.prototype.set = function (item) {
var key = this._key(item);
if (!key) {
// Ignore empty keys.
return;
}
var previous = this._data[key];
if (previous) {
unset.call(this, previous, key);
}
this._data[key] = item;
forEach(this._indexes, setItemToIndex, item);
};
Collection.prototype.unset = function (item) {
var key = this._key(item);
item = this._data[key];
if (!item) {
return;
}
unset.call(this, item, this._key(item));
};
Collection.prototype.setMultiple = function (items) {
forEach(items, this.set, this);
};
Collection.prototype.unsetMultiple = function (items) {
forEach(items, this.unset, this);
};
//====================================================================
function createCollection(opts) {
return new Collection(opts);
}
module.exports = createCollection;

View File

@@ -0,0 +1,9 @@
'use strict'
// ===================================================================
var makeError = require('make-error')
// ===================================================================
module.exports = makeError('ConnectionError')

View File

@@ -0,0 +1,36 @@
'use strict'
var xoLib = require('./')
var xo = new xoLib.Xo({
url: 'localhost:9000'
})
xo.call('acl.get', {}).then(function (result) {
console.log('baz', result)
}).catch(function (error) {
console.log('error', error)
})
xo.signIn({
email: 'admin@admin.net',
password: 'admin'
}).then(function () {
console.log('foo', xo.user)
}).catch(function (error) {
console.log('error', error)
})
xo.signIn({
email: 'tom',
password: 'tom'
}).then(function () {
console.log('bar', xo.user)
}).catch(function (error) {
console.log('error', error)
})
xo.call('acl.get', {}).then(function (result) {
console.log('plop', result)
}).catch(function (error) {
console.log('error', error)
})

View File

@@ -1,2 +0,0 @@
/node_modules/
bundle.js

View File

@@ -1,7 +0,0 @@
> Minimalist browser example.
```
> npm install
> npm run build
> open index.html
```

View File

@@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>xo-lib</title>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
'use strict';
//====================================================================
var Xo = require('..');
//====================================================================
var xo = new Xo('localhost:9000');
xo.call('system.getMethodsInfo').then(function (methods) {
console.log(methods);
});

View File

@@ -1,11 +0,0 @@
{
"private": true,
"scripts": {
"build": "browserify --outfile bundle.js index.js",
"dev": "watchify --debug --outfile bundle.js index.js"
},
"devDependencies": {
"browserify": "^8.1.3",
"watchify": "^2.3.0"
}
}

View File

@@ -0,0 +1,21 @@
'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

View File

@@ -1,38 +1,48 @@
'use strict';
'use strict'
//====================================================================
// ===================================================================
var expect = require('must');
var expect = require('must')
//====================================================================
// ===================================================================
/* eslint-env mocha */
describe('fixUrl()', function () {
var fixUrl = require('./').fixUrl;
var fixUrl = require('./fix-url')
describe('protocol', function () {
it('is added if missing', function () {
expect(fixUrl('localhost/api/')).to.equal('ws://localhost/api/');
});
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/');
});
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/');
});
});
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/');
});
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/');
});
});
});
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')
})
})
})

View File

@@ -1,320 +1,7 @@
'use strict';
//====================================================================
var Bluebird = require('bluebird');
var EventEmitter = require('events').EventEmitter;
var inherits = require('util').inherits;
var jsonRpc = require('json-rpc');
var makeError = require('make-error');
var MethodNotFound = require('json-rpc/errors').MethodNotFound;
var WebSocket = require('ws');
var createCollection = require('./collection');
//====================================================================
'use strict'
// Expose Bluebird for now to ease integration (e.g. with Angular.js).
exports.setScheduler = Bluebird.setScheduler;
exports.setScheduler = require('bluebird').setScheduler
//====================================================================
function makeDeferred() {
var resolve, reject;
var promise = new Bluebird(function (resolve_, reject_) {
resolve = resolve_;
reject = reject_;
});
return {
promise: promise,
reject: reject,
resolve: resolve,
};
}
function startsWith(string, target) {
return (string.lastIndexOf(target, 0) === 0);
}
//====================================================================
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;
}
//====================================================================
// 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 rest = matches[2];
return [
isSecure ? 'wss' : 'ws',
'://',
rest,
'/api/',
].join('');
}
exports.fixUrl = fixUrl;
//====================================================================
function getCurrentUrl() {
/* global window: false */
if (typeof window === undefined) {
throw new Error('cannot get current URL');
}
return window.location.host + window.location.pathname;
}
//====================================================================
var ConnectionLost = makeError('ConnectionLost');
// 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 WebSocket.
this._socket = null;
// The JSON-RPC server.
var this_ = this;
this._jsonRpc = jsonRpc.createServer(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(JSON.stringify(message));
});
}
inherits(Api, EventEmitter);
Api.prototype.close = function () {
if (this._socket) {
this._socket.close();
}
};
Api.prototype.connect = Bluebird.method(function () {
if (this._socket) {
return;
}
var deferred = makeDeferred();
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('message', function (message) {
this_._jsonRpc.write(message.data);
});
socket.addEventListener('close', function () {
this_._socket = null;
this_._jsonRpc.failPendingRequests(new ConnectionLost());
// Only emit this event if connected before.
if (deferred.promise.isFulfilled()) {
this_.emit('disconnected');
}
});
socket.addEventListener('error', function (error) {
// Fails the connect promise if possible.
deferred.reject(error);
});
return deferred.promise;
});
Api.prototype.call = function (method, params) {
var jsonRpc = this._jsonRpc;
return this.connect().then(function () {
return jsonRpc.request(method, params);
});
};
exports.Api = Api;
//====================================================================
var objectsOptions = {
indexes: [
'ref',
'type',
'UUID',
],
key: function (item) {
return item.UUID || item.ref;
},
};
function tryConnect() {
/* jshint validthis: true */
this.status = 'connecting';
return this._api.connect().bind(this).catch(function () {
this.status = 'disconnected';
var delay = this._backOff.next().value;
return Bluebird.delay(delay).bind(this).then(tryConnect);
});
}
function onSuccessfulConnection() {
/* jshint validthis: true */
// Reset back off.
this._backOff = fibonacci(1e3);
// FIXME: session.signIn() should work with both token and password.
return this._api.call(
this._auth.token ?
'session.signInWithToken' :
'session.signInWithPassword',
this._auth
).bind(this).then(function (user) {
this.user = user;
this.status = 'connected';
this._api.call('xo.getAllObjects').bind(this).then(function (objects) {
this.objects.clear();
this.objects.setMultiple(objects);
});
});
}
function onFailedConnection() {
/* jshint validthis: true */
this.status = 'disconnected';
}
function connect() {
/* jshint validthis: true */
if (this._connection) {
return this._connection;
}
this._connection = tryConnect.call(this).then(
onSuccessfulConnection, onFailedConnection
);
return this._connection;
}
// High level interface to Xo.
//
// Handle auto-reconnect, sign in & objects cache.
function Xo(opts) {
var self = this;
this._api = new Api(opts.url);
this._auth = opts.auth;
this._backOff = fibonacci(1e3);
this.objects = createCollection(objectsOptions);
this.status = 'disconnected';
this.user = null;
// Promise representing the connection status.
this._connection = null;
self._api.on('disconnected', function () {
// Automatically reconnect.
self._connection = null;
connect.call(self);
});
self._api.on('notification', function (notification) {
if (notification.method !== 'all') {
return;
}
var method = (
notification.params.type === 'exit' ?
'unset' :
'set'
) + 'Multiple';
self.objects[method](notification.params.items);
});
// Bootstrap the connection.
connect.call(this);
}
Xo.prototype.call = function (method, params) {
// Prevent session.*() from being because it may interfere
// with this class session management.
if (startsWith(method, 'session.')) {
throw new Error('session.*() methods are disabled from this interface');
}
return connect.call(this).then(function () {
var self = this;
return this._api.call(method, params).catch(ConnectionLost, function () {
// Retry automatically.
return self.call(method, params);
});
});
};
exports.Xo = Xo;
exports.Api = require('./api')
exports.Xo = require('./xo')

View File

@@ -1,6 +1,6 @@
{
"name": "xo-lib",
"version": "0.5.1",
"version": "0.7.3",
"description": "Library to connect to XO-Server",
"keywords": [
"xen",
@@ -19,23 +19,25 @@
"node": ">=0.8.0"
},
"scripts": {
"test": "mocha index.spec.js"
"test": "standard && mocha '*.spec.js'"
},
"files": [
"index.js",
"collection.js"
],
"dependencies": {
"@julien-f/json-rpc": "^0.3.5",
"bluebird": "^2.9.6",
"json-rpc": "git://github.com/julien-f/js-json-rpc",
"event-to-promise": "^0.3.3",
"exec-promise": "^0.5.1",
"lodash.assign": "^3.0.0",
"lodash.foreach": "^3.0.1",
"lodash.indexof": "^3.0.0",
"lodash.isstring": "^3.0.0",
"lodash.startswith": "^3.0.0",
"make-error": "^0.3.0",
"ws": "^0.7.1"
"pw": "0.0.4",
"ws": "^0.8.0",
"xo-collection": "^0.3.1"
},
"devDependencies": {
"mocha": "^2.1.0",
"must": "^0.12.0"
"must": "^0.12.0",
"standard": "*"
}
}

View File

@@ -0,0 +1,9 @@
'use strict'
// ===================================================================
var makeError = require('make-error')
// ===================================================================
module.exports = makeError('SessionError')

279
packages/xo-lib/xo.js Normal file
View File

@@ -0,0 +1,279 @@
'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) {
// 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')
)
}
return this._session.bind(this).then(function () {
return this._api.call(method, params)
}).catch(ConnectionError, SessionError, function () {
// Automatically requeue this call.
return this.call(method, params)
})
}
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(
credentials.token ?
'session.signInWithToken' :
'session.signInWithPassword',
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