websocket-as-promised
Advanced tools
Comparing version 0.1.3 to 0.1.4
@@ -13,2 +13,3 @@ /** | ||
var Channel = require('chnl'); | ||
var Pendings = require('pendings'); | ||
@@ -19,2 +20,8 @@ | ||
var DEFAULT_OPTIONS = { | ||
idProp: 'id', | ||
timeout: 0, | ||
WebSocket: typeof WebSocket !== 'undefined' ? WebSocket : null | ||
}; | ||
module.exports = function () { | ||
@@ -26,2 +33,3 @@ /** | ||
* @param {String} [options.idProp="id"] id property name attached to each message | ||
* @param {Object} [options.timeout=0] default timeout for requests | ||
* @param {Object} [options.WebSocket=WebSocket] custom WebSocket constructor | ||
@@ -32,6 +40,5 @@ */ | ||
options = options || {}; | ||
this._idProp = options.idProp || 'id'; | ||
this._WebSocket = options.WebSocket || WebSocket; | ||
this._pendings = new Pendings(); | ||
this._options = Object.assign({}, DEFAULT_OPTIONS, options); | ||
this._pendings = new Pendings({ timeout: this._options.timeout }); | ||
this._onMessage = new Channel(); | ||
this._ws = null; | ||
@@ -61,14 +68,14 @@ } | ||
return this._pendings.set(OPENING_ID, function () { | ||
_this._ws = new _this._WebSocket(url); | ||
_this._ws = new _this._options.WebSocket(url); | ||
_this._ws.addEventListener('open', function (event) { | ||
return _this._onOpen(event); | ||
return _this._handleOpen(event); | ||
}); | ||
_this._ws.addEventListener('message', function (event) { | ||
return _this._onMessage(event); | ||
return _this._handleMessage(event); | ||
}); | ||
_this._ws.addEventListener('error', function (event) { | ||
return _this._onError(event); | ||
return _this._handleError(event); | ||
}); | ||
_this._ws.addEventListener('close', function (event) { | ||
return _this._onClose(event); | ||
return _this._handleClose(event); | ||
}); | ||
@@ -79,5 +86,7 @@ }); | ||
/** | ||
* Send data and wait for response containing `id` property | ||
* Performs request and resolves after response with corresponding `id`. | ||
* | ||
* @param {Object} data | ||
* @param {Object} [options] | ||
* @param {Number} [options.timeout] | ||
* @returns {Promise} | ||
@@ -87,17 +96,16 @@ */ | ||
}, { | ||
key: 'send', | ||
value: function send(data) { | ||
key: 'request', | ||
value: function request(data, options) { | ||
var _this2 = this; | ||
return this._pendings.add(function (id) { | ||
if (!data || (typeof data === 'undefined' ? 'undefined' : _typeof(data)) !== 'object') { | ||
throw new Error('WebSocket data should be a plain object, got ' + data); | ||
} | ||
if (data[_this2._idProp] !== undefined) { | ||
throw new Error('WebSocket data should not contain system property: ' + _this2._idProp); | ||
} | ||
data[_this2._idProp] = id; | ||
if (!data || (typeof data === 'undefined' ? 'undefined' : _typeof(data)) !== 'object') { | ||
return Promise.reject(new Error('WebSocket data should be a plain object, got ' + data)); | ||
} | ||
var idProp = this._options.idProp; | ||
var fn = function fn(id) { | ||
data[idProp] = id; | ||
var dataStr = JSON.stringify(data); | ||
_this2._ws.send(dataStr); | ||
}); | ||
}; | ||
return data[idProp] === undefined ? this._pendings.add(fn, options) : this._pendings.set(data[idProp], fn, options); | ||
} | ||
@@ -121,34 +129,30 @@ | ||
}, { | ||
key: '_onOpen', | ||
value: function _onOpen(event) { | ||
key: '_handleOpen', | ||
value: function _handleOpen(event) { | ||
this._pendings.resolve(OPENING_ID, event); | ||
} | ||
}, { | ||
key: '_onMessage', | ||
value: function _onMessage(event) { | ||
key: '_handleMessage', | ||
value: function _handleMessage(event) { | ||
if (event.data) { | ||
var data = JSON.parse(event.data); | ||
var id = data && data[this._idProp]; | ||
var id = data && data[this._options.idProp]; | ||
if (id) { | ||
this._pendings.resolve(id, data); | ||
} | ||
this._onMessage.dispatch(data); | ||
} | ||
} | ||
}, { | ||
key: '_onError', | ||
value: function _onError(event) { | ||
if (this._pendings.has(OPENING_ID)) { | ||
this._pendings.reject(OPENING_ID, event); | ||
} | ||
if (this._pendings.has(CLOSING_ID)) { | ||
this._pendings.reject(CLOSING_ID, event); | ||
} | ||
key: '_handleError', | ||
value: function _handleError(event) { | ||
this._pendings.reject(OPENING_ID, event); | ||
this._pendings.reject(CLOSING_ID, event); | ||
} | ||
}, { | ||
key: '_onClose', | ||
value: function _onClose(event) { | ||
key: '_handleClose', | ||
value: function _handleClose(event) { | ||
this._ws = null; | ||
if (this._pendings.has(CLOSING_ID)) { | ||
this._pendings.resolve(CLOSING_ID, event); | ||
} | ||
this._pendings.resolve(CLOSING_ID, event); | ||
this._pendings.rejectAll(new Error('Connection closed.')); | ||
} | ||
@@ -160,2 +164,15 @@ }, { | ||
} | ||
/** | ||
* OnMessage channel with `.addListener` / `.removeListener` methods. | ||
* @see https://github.com/vitalets/chnl | ||
* | ||
* @returns {Channel} | ||
*/ | ||
}, { | ||
key: 'onMessage', | ||
get: function get() { | ||
return this._onMessage; | ||
} | ||
}]); | ||
@@ -162,0 +179,0 @@ |
{ | ||
"name": "websocket-as-promised", | ||
"version": "0.1.3", | ||
"version": "0.1.4", | ||
"description": "Promise-based WebSocket wrapper", | ||
@@ -39,3 +39,4 @@ "author": { | ||
"dependencies": { | ||
"pendings": "^0.1.4" | ||
"chnl": "^0.2.5", | ||
"pendings": "^0.1.5" | ||
}, | ||
@@ -42,0 +43,0 @@ "devDependencies": { |
@@ -49,2 +49,3 @@ # websocket-as-promised | ||
* @param {String} [options.idProp="id"] id property name attached to each message | ||
* @param {Object} [options.timeout=0] default timeout for requests | ||
* @param {Object} [options.WebSocket=WebSocket] custom WebSocket constructor | ||
@@ -62,8 +63,10 @@ */ | ||
``` | ||
#### .send(data) | ||
#### .request(data, options) | ||
``` | ||
/** | ||
* Send data and wait for response containing `id` property | ||
* Performs request and resolves after response with corresponding `id`. | ||
* | ||
* @param {Object} data | ||
* @param {Object} [options] | ||
* @param {Number} [options.timeout] | ||
* @returns {Promise} | ||
@@ -81,2 +84,12 @@ */ | ||
#### .onMessage | ||
``` | ||
/** | ||
* OnMessage channel with `.addListener` / `.removeListener` methods. | ||
* @see https://github.com/vitalets/chnl | ||
* | ||
* @returns {Channel} | ||
*/ | ||
``` | ||
#### .ws | ||
@@ -83,0 +96,0 @@ ``` |
@@ -7,2 +7,3 @@ /** | ||
const Channel = require('chnl'); | ||
const Pendings = require('pendings'); | ||
@@ -13,2 +14,8 @@ | ||
const DEFAULT_OPTIONS = { | ||
idProp: 'id', | ||
timeout: 0, | ||
WebSocket: typeof WebSocket !== 'undefined' ? WebSocket : null, | ||
}; | ||
module.exports = class WebSocketAsPromised { | ||
@@ -20,9 +27,9 @@ /** | ||
* @param {String} [options.idProp="id"] id property name attached to each message | ||
* @param {Object} [options.timeout=0] default timeout for requests | ||
* @param {Object} [options.WebSocket=WebSocket] custom WebSocket constructor | ||
*/ | ||
constructor(options) { | ||
options = options || {}; | ||
this._idProp = options.idProp || 'id'; | ||
this._WebSocket = options.WebSocket || WebSocket; | ||
this._pendings = new Pendings(); | ||
this._options = Object.assign({}, DEFAULT_OPTIONS, options); | ||
this._pendings = new Pendings({timeout: this._options.timeout}); | ||
this._onMessage = new Channel(); | ||
this._ws = null; | ||
@@ -41,2 +48,12 @@ } | ||
/** | ||
* OnMessage channel with `.addListener` / `.removeListener` methods. | ||
* @see https://github.com/vitalets/chnl | ||
* | ||
* @returns {Channel} | ||
*/ | ||
get onMessage() { | ||
return this._onMessage; | ||
} | ||
/** | ||
* Open WebSocket connection | ||
@@ -49,7 +66,7 @@ * | ||
return this._pendings.set(OPENING_ID, () => { | ||
this._ws = new this._WebSocket(url); | ||
this._ws.addEventListener('open', event => this._onOpen(event)); | ||
this._ws.addEventListener('message', event => this._onMessage(event)); | ||
this._ws.addEventListener('error', event => this._onError(event)); | ||
this._ws.addEventListener('close', event => this._onClose(event)); | ||
this._ws = new this._options.WebSocket(url); | ||
this._ws.addEventListener('open', event => this._handleOpen(event)); | ||
this._ws.addEventListener('message', event => this._handleMessage(event)); | ||
this._ws.addEventListener('error', event => this._handleError(event)); | ||
this._ws.addEventListener('close', event => this._handleClose(event)); | ||
}); | ||
@@ -59,19 +76,22 @@ } | ||
/** | ||
* Send data and wait for response containing `id` property | ||
* Performs request and resolves after response with corresponding `id`. | ||
* | ||
* @param {Object} data | ||
* @param {Object} [options] | ||
* @param {Number} [options.timeout] | ||
* @returns {Promise} | ||
*/ | ||
send(data) { | ||
return this._pendings.add(id => { | ||
if (!data || typeof data !== 'object') { | ||
throw new Error(`WebSocket data should be a plain object, got ${data}`); | ||
} | ||
if (data[this._idProp] !== undefined) { | ||
throw new Error(`WebSocket data should not contain system property: ${this._idProp}`); | ||
} | ||
data[this._idProp] = id; | ||
request(data, options) { | ||
if (!data || typeof data !== 'object') { | ||
return Promise.reject(new Error(`WebSocket data should be a plain object, got ${data}`)); | ||
} | ||
const idProp = this._options.idProp; | ||
const fn = id => { | ||
data[idProp] = id; | ||
const dataStr = JSON.stringify(data); | ||
this._ws.send(dataStr); | ||
}); | ||
}; | ||
return data[idProp] === undefined | ||
? this._pendings.add(fn, options) | ||
: this._pendings.set(data[idProp], fn, options); | ||
} | ||
@@ -88,31 +108,27 @@ | ||
_onOpen(event) { | ||
_handleOpen(event) { | ||
this._pendings.resolve(OPENING_ID, event); | ||
} | ||
_onMessage(event) { | ||
_handleMessage(event) { | ||
if (event.data) { | ||
const data = JSON.parse(event.data); | ||
const id = data && data[this._idProp]; | ||
const id = data && data[this._options.idProp]; | ||
if (id) { | ||
this._pendings.resolve(id, data); | ||
} | ||
this._onMessage.dispatch(data); | ||
} | ||
} | ||
_onError(event) { | ||
if (this._pendings.has(OPENING_ID)) { | ||
this._pendings.reject(OPENING_ID, event); | ||
} | ||
if (this._pendings.has(CLOSING_ID)) { | ||
this._pendings.reject(CLOSING_ID, event); | ||
} | ||
_handleError(event) { | ||
this._pendings.reject(OPENING_ID, event); | ||
this._pendings.reject(CLOSING_ID, event); | ||
} | ||
_onClose(event) { | ||
_handleClose(event) { | ||
this._ws = null; | ||
if (this._pendings.has(CLOSING_ID)) { | ||
this._pendings.resolve(CLOSING_ID, event); | ||
} | ||
this._pendings.resolve(CLOSING_ID, event); | ||
this._pendings.rejectAll(new Error('Connection closed.')); | ||
} | ||
}; |
@@ -12,2 +12,6 @@ 'use strict'; | ||
function sleep(ms) { | ||
return new Promise(resolve => setTimeout(resolve, ms)); | ||
} | ||
describe('WebSocketAsPromised', function () { | ||
@@ -37,13 +41,27 @@ | ||
it('should open connection', function () { | ||
return this.wsp.open(this.url) | ||
.then(event => assert.equal(event.type, 'open')); | ||
const res = this.wsp.open(this.url); | ||
return assert.eventually.propertyVal(res, 'type', 'open'); | ||
}); | ||
it('should send and receive data with id', function () { | ||
return this.wsp.open(this.url) | ||
.then(() => this.wsp.send({foo: 'bar'})) | ||
.then(data => { | ||
assert.equal(data.foo, 'bar'); | ||
assert.property(data, 'id'); | ||
it('should request and resolve with generated id', function () { | ||
const res = this.wsp.open(this.url).then(() => this.wsp.request({foo: 'bar'})); | ||
return Promise.all([ | ||
assert.eventually.propertyVal(res, 'foo', 'bar'), | ||
assert.eventually.property(res, 'id') | ||
]); | ||
}); | ||
it('should request and resolve with specified id', function () { | ||
const res = this.wsp.open(this.url).then(() => this.wsp.request({foo: 'bar', id: 1})); | ||
return assert.eventually.propertyVal(res, 'id', 1); | ||
}); | ||
it('should not resolve/reject for response without ID', function () { | ||
let a = 0; | ||
const res = this.wsp.open(this.url) | ||
.then(() => { | ||
this.wsp.request({noId: true}).then(() => a = a + 1, () => {}); | ||
return sleep(100).then(() => a); | ||
}); | ||
return assert.eventually.equal(res, 0); | ||
}); | ||
@@ -53,8 +71,17 @@ | ||
const CLOSE_NORMAL = 1000; | ||
return this.wsp.open(this.url) | ||
.then(() => this.wsp.close()) | ||
.then(event => assert.equal(event.code, CLOSE_NORMAL)); | ||
const res = this.wsp.open(this.url).then(() => this.wsp.close()); | ||
return assert.eventually.propertyVal(res, 'code', CLOSE_NORMAL); | ||
}); | ||
it('should reject for invalid url', function () { | ||
it('should reject all pending requests on close', function () { | ||
let a = ''; | ||
const res = this.wsp.open(this.url) | ||
.then(() => { | ||
this.wsp.request({noId: true}).catch(e => a = e.message); | ||
return sleep(10).then(() => this.wsp.close()).then(() => a); | ||
}); | ||
return assert.eventually.equal(res, 'Connection closed.'); | ||
}); | ||
it('should reject connection for invalid url', function () { | ||
const res = this.wsp.open('abc'); | ||
@@ -65,10 +92,40 @@ return assert.isRejected(res, 'You must specify a full WebSocket URL, including protocol.'); | ||
it('should customize idProp', function () { | ||
this.wsp = new WebSocketAsPromised({WebSocket: W3CWebSocket, idProp: 'myId'}); | ||
return this.wsp.open(this.url) | ||
.then(() => this.wsp.send({foo: 'bar'})) | ||
.then(data => { | ||
assert.equal(data.foo, 'bar'); | ||
assert.property(data, 'myId'); | ||
}); | ||
const wsp = new WebSocketAsPromised({WebSocket: W3CWebSocket, idProp: 'myId'}); | ||
const res = wsp.open(this.url).then(() => wsp.request({foo: 'bar'})); | ||
return Promise.all([ | ||
assert.eventually.propertyVal(res, 'foo', 'bar'), | ||
assert.eventually.property(res, 'myId'), | ||
]); | ||
}); | ||
it('should dispatch data via onMessage channel', function () { | ||
const wsp = new WebSocketAsPromised({WebSocket: W3CWebSocket}); | ||
const res = new Promise(resolve => { | ||
wsp.onMessage.addListener(resolve); | ||
wsp.open(this.url).then(() => wsp.request({foo: 'bar'})); | ||
}); | ||
return assert.eventually.propertyVal(res, 'foo', 'bar'); | ||
}); | ||
describe('timeout', function () { | ||
it('should reject request after timeout', function () { | ||
const wsp = new WebSocketAsPromised({WebSocket: W3CWebSocket, timeout: 50}); | ||
const res = wsp.open(this.url).then(() => wsp.request({foo: 'bar', delay: 100})); | ||
return assert.isRejected(res, 'Promise rejected by timeout (50 ms)'); | ||
}); | ||
it('should resolve request before timeout', function () { | ||
const wsp = new WebSocketAsPromised({WebSocket: W3CWebSocket, timeout: 100}); | ||
const res = wsp.open(this.url).then(() => wsp.request({foo: 'bar', delay: 50})); | ||
return assert.eventually.propertyVal(res, 'foo', 'bar'); | ||
}); | ||
it('should reject request after custom timeout', function () { | ||
const wsp = new WebSocketAsPromised({WebSocket: W3CWebSocket, timeout: 100}); | ||
const options = {timeout: 50}; | ||
const res = wsp.open(this.url).then(() => wsp.request({foo: 'bar', delay: 70}, options)); | ||
return assert.isRejected(res, 'Promise rejected by timeout (50 ms)'); | ||
}); | ||
}); | ||
}); |
@@ -24,8 +24,3 @@ 'use strict'; | ||
if (message.type === 'utf8') { | ||
const data = JSON.parse(message.utf8Data); | ||
if (data.error) { | ||
// todo: trigger error | ||
} else { | ||
connection.sendUTF(message.utf8Data); | ||
} | ||
handleUTF8Message(connection, message); | ||
} else if (message.type === 'binary') { | ||
@@ -43,2 +38,16 @@ connection.sendBytes(message.binaryData); | ||
}); | ||
function handleUTF8Message(connection, message) { | ||
const data = JSON.parse(message.utf8Data); | ||
if (data.nonJSONResponse) { | ||
connection.sendUTF('non JSON response'); | ||
} else if (data.noId) { | ||
delete data.id; | ||
connection.sendUTF(JSON.stringify(data)); | ||
} else if (data.delay) { | ||
setTimeout(() => connection.sendUTF(message.utf8Data), data.delay); | ||
} else { | ||
connection.sendUTF(message.utf8Data); | ||
} | ||
} | ||
}; | ||
@@ -45,0 +54,0 @@ |
18377
421
106
2
+ Addedchnl@^0.2.5
+ Addedchnl@0.2.5(transitive)
Updatedpendings@^0.1.5