From e862f7e3912fcf4f4747629c9fe94ade5013232a Mon Sep 17 00:00:00 2001 From: Terry Sahaidak Date: Sun, 30 Apr 2017 21:25:58 +0300 Subject: [PATCH 1/5] Setup browser-faye for realtime module --- app/api/faye.js | 155 +++++++++++++++ app/modules/app.js | 24 +-- app/modules/old.js | 406 ++++++++++++++++++++++++++++++++++++++++ app/modules/realtime.js | 337 +++++++++++---------------------- app/modules/rooms.js | 2 +- package.json | 8 +- 6 files changed, 688 insertions(+), 244 deletions(-) create mode 100644 app/api/faye.js create mode 100644 app/modules/old.js diff --git a/app/api/faye.js b/app/api/faye.js new file mode 100644 index 0000000..a9850a7 --- /dev/null +++ b/app/api/faye.js @@ -0,0 +1,155 @@ +import Faye from 'faye' +const noop = () => {} + +class ClientAuthExt { + constructor(token) { + this._token = token + } + + outgoing(message, cb) { + if (message.channel === '/meta/handshake') { + if (!message.ext) { message.ext = {}; } + message.ext.token = this._token; + } + + cb(message); + } + + incoming(message, cb) { + if (message.channel === '/meta/handshake') { + if (message.successful) { + console.log('Successfuly subscribed: ', message); + } else { + console.log('Something went wrong: ', message.error); + } + } + + cb(message) + } +} + +class LogExt { + outgoing(message, cb) { + console.log('Log outgoing message: ', message) + cb(message) + } + + incoming(message, cb) { + console.log('Log incoming message: ', message) + cb(message) + } +} + +class SnapshotExt { + constructor(fn) { + this._handler = fn + } + incoming(message, cb) { + if (message.channel === '/meta/subscribe' && message.ext && message.ext.snapshot) { + this._handler(message) + } + + cb(message) + } +} + + +export default class HalleyClient { + constructor({token, snapshotHandler}) { + this._client = new Faye.Client('https://ws.gitter.im/faye', { + timeout: 60, + retry: 1, + interval: 0 + }) + this._token = token + this._snapshotHandler = snapshotHandler + this._subsciptions = [] + } + + setToken(token) { + this._token = token + } + + setSnapshotHandler(fn) { + this._snapshotHandler = fn + } + + setup() { + if (!this._token) { + throw new Error('You need to add token') + } + + this._snapshotExt = new SnapshotExt(this._snapshotHandler || noop) + this._authExt = new ClientAuthExt(this._token) + } + + create() { + if (!this._token) { + throw new Error('You need to add token') + } + this._client.addExtension(this._authExt) + this._client.addExtension(this._snapshotExt) + this._client.addExtension(new LogExt()) + } + + subscribe({type, url, handler}) { + return new Promise((res, rej) => { + if (this._checkSubscriptionAlreadyExist({type, url})) { + rej(`Subscription with type ${type} and url ${url} already exist.`) + } + + const subscriptionObject = this._client.subscribe(url, handler) + + subscriptionObject + .then(() => { + this._subsciptions.push({ + type, + url, + handler, + subscriptionObject + }) + res(true) + }) + // .catch(err => { + // rej(err) + // }) + }) + } + + unsubscribe({type, url}) { + return new Promise((res, rej) => { + const subscription = this._findSubscription({type, url}) + if (!subscription) { + rej(`There is no subscription with type ${type} and url ${url}`) + } else { + subscription.subscriptionObject.unsubscribe() + .then(() => { + this._removeSubscription(subscription) + res(true) + }) + // .catch(err => rej(err)) + } + }) + } + + _checkSubscriptionAlreadyExist(subscriptionOptions) { + const subscription = this._findSubscription(subscriptionOptions) + return !!subscription + } + + _findSubscription({type, url}) { + return this._subsciptions.find( + item => item.type === type && item.url === url + ) + } + + _removeSubscription({type, url}) { + const index = this._subsciptions.indexOf( + item => item.type === type && item.url === url + ) + + if (index !== -1) { + this._subsciptions.splice(index, 1) + } + } +} diff --git a/app/modules/app.js b/app/modules/app.js index cbfcf44..e7b14dc 100644 --- a/app/modules/app.js +++ b/app/modules/app.js @@ -4,7 +4,7 @@ import {getRooms, getSuggestedRooms} from './rooms' import {initializeUi} from './ui' import {NetInfo, AppState} from 'react-native' import { - setupFayeEvents, + // setupFayeEvents, setupFaye, onNetStatusChangeFaye, subscribeToChatMessages, @@ -43,7 +43,7 @@ export function init() { return } - dispatch(setupFayeEvents()) + // dispatch(setupFayeEvents()) dispatch({ type: INITIALIZED, token }) // getting base current user's information @@ -75,7 +75,7 @@ function setupNetStatusListener() { NetInfo.isConnected.addEventListener('change', async status => { dispatch({type: CHANGE_NET_STATUS, payload: status}) - await dispatch(onNetStatusChangeFaye(status)) + // await dispatch(onNetStatusChangeFaye(status)) } ); } @@ -93,15 +93,15 @@ function setupAppStatusListener() { if (!token.length || !netStatus) { return } - const {fayeConnected} = getState().app - if (!fayeConnected) { - await setupFaye() - } - - const {activeRoom} = getState().rooms - if (!!activeRoom) { - dispatch(subscribeToChatMessages(activeRoom)) - } + // const {fayeConnected} = getState().app + // if (!fayeConnected) { + // await setupFaye() + // } + + // const {activeRoom} = getState().rooms + // if (!!activeRoom) { + // dispatch(subscribeToChatMessages(activeRoom)) + // } dispatch(subscribeToRooms()) } catch (error) { console.log(error) diff --git a/app/modules/old.js b/app/modules/old.js new file mode 100644 index 0000000..f7bc498 --- /dev/null +++ b/app/modules/old.js @@ -0,0 +1,406 @@ +import FayeGitter from '../../libs/react-native-gitter-faye/index' +import {DeviceEventEmitter, NativeEventEmitter, Platform} from 'react-native' +import {updateRoomState, receiveRoomsSnapshot} from './rooms' +import {appendMessages, updateMessageRealtime, receiveRoomMessagesSnapshot} from './messages' +import {receiveRoomEventsSnapshot} from './activity' +import {receiveReadBySnapshot} from './readBy' + +/** + * Constants + */ + +export const FAYE_CONNECTING = 'realtime/FAYE_CONNECTING' +export const FAYE_CONNECT = 'realtime/FAYE_CONNECT' +export const ROOMS_SUBSCRIBED = 'realtime/ROOMS_SUBSCRIBED' +export const ROOMS_UNSUBSCRIBED = 'realtime/ROOMS_UNSUBSCRIBED' +export const SUBSCRIBE_TO_CHAT_MESSAGES = 'realtime/SUBSCRIBE_TO_CHAT_MESSAGES' +export const UNSUBSCRIBE_TO_CHAT_MESSAGES = 'realtime/UNSUBSCRIBE_TO_CHAT_MESSAGES' +export const SUBSCRIBE_TO_ROOM_EVENTS = 'realtime/SUBSCRIBE_TO_ROOM_EVENTS' +export const UNSUBSCRIBE_TO_ROOM_EVENTS = 'realtime/UNSUBSCRIBE_TO_ROOM_EVENTS' +export const SUBSCRIBE_TO_READ_BY = 'realtime/SUBSCRIBE_TO_READ_BY' +export const UNSUBSCRIBE_FROM_READ_BY = 'realtime/UNSUBSCRIBE_FROM_READ_BY' +export const PUSH_SUBSCRIPTION = 'realtime/PUSH_SUBSCRIPTION' +export const DELETE_SUBSCRIPTION = 'realtime/DELETE_SUBSCRIPTION' +export const SUBSCRIBED_TO_CHANNELS = 'realtime/SUBSCRIBED_TO_CHANNELS' + +const roomsRegx = /\/api\/v1\/user\/[a-f\d]{24}\/rooms/ +const messagesRegx = /\/api\/v1\/rooms\/[a-f\d]{24}\/chatMessages/ +const messagesRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/chatMessages/ +const eventsRegx = /\/api\/v1\/rooms\/[a-f\d]{24}\/events/ +const eventsRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/events/ +const readByRegx = /\/api\/v1\/rooms\/[a-f\d]{24}\/chatMessages\/[a-f\d]{24}\/readBy/ +const readByRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/chatMessages\/([a-f\d]{24})\/readBy/ + + +/** + * Actions + */ + +export function setupFaye() { + return async (dispatch, getState) => { + console.log('RECONNECT TO FAYE') + FayeGitter.setAccessToken(getState().auth.token) + FayeGitter.create() + FayeGitter.logger() + try { + dispatch({type: FAYE_CONNECTING}) + const result = await FayeGitter.connect() + dispatch({type: FAYE_CONNECT, payload: result}) + // dispatch(subscribeToChannels()) + } catch (err) { + console.log(err) // eslint-disable-line no-console + } + } +} + +export function checkFayeConnection() { + return async (dispatch, getState) => { + try { + const connectionStatus = await FayeGitter.checkConnectionStatus() + console.log('CONNECTION_STATUS', connectionStatus) + if (!connectionStatus) { + await dispatch(setupFaye()) + } + } catch (error) { + console.error(error.message) + } + } +} + +/** + * Handler which handles net status changes and reconnects to faye if needed + */ + +export function onNetStatusChangeFaye(status) { + return async (dispatch, getState) => { + const {isFayeConnecting} = getState().app + if (isFayeConnecting) { + return + } + const connectionStatus = await FayeGitter.checkConnectionStatus() + if (!status && connectionStatus) { + dispatch({type: FAYE_CONNECT, payload: status}) + } + try { + if (status && !connectionStatus) { + await dispatch(setupFaye()) + } + } catch (error) { + console.warn(error.message) + } + } +} + +/** + * Setup faye events + */ + +export function setupFayeEvents() { + return (dispatch, getState) => { + const EventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(FayeGitter) : DeviceEventEmitter + EventEmitter + .addListener('FayeGitter:Connected', log => { + console.log('CONNECTED') + dispatch(subscribeToChannels()) + }) + EventEmitter + .addListener('FayeGitter:onDisconnected', log => { + console.log(log) // eslint-disable-line no-console + dispatch(setupFaye()) + }) + + EventEmitter + .addListener('FayeGitter:onFailedToCreate', log => { + console.log(log) // eslint-disable-line no-console + const {online} = getState().app + if (online === true) { + dispatch(setupFaye()) + } + }) + EventEmitter + .addListener('FayeGitter:Message', event => { + dispatch(parseEvent(event)) + }) + EventEmitter + .addListener('FayeGitter:log', event => { + dispatch(parseSnapshotEvent(event)) + }) + EventEmitter + .addListener('FayeGitter:SubscribtionFailed', log => console.log(log)) // eslint-disable-line no-console + EventEmitter + .addListener('FayeGitter:Subscribed', event => { + console.log('SUBSCRIBED', event) // eslint-disable-line no-console + if (Platform.OS === 'ios') { + try { + const {channel, ext} = event + const snapshot = JSON.parse(ext).snapshot + if (typeof channel !== 'undefined' && typeof snapshot !== 'undefined') { + dispatch(parseSnapshotForChannel(channel, snapshot)) + } + } catch (error) { + console.warn(error.message) + } + } + }) + EventEmitter + .addListener('FayeGitter:Unsubscribed', log => console.log('UNSUBSCRIBED', log)) // eslint-disable-line no-console + } +} + +export function removeFayeEvents() { + return (dispatch) => { + DeviceEventEmitter.removeEventListener('FayeGitter:Connected') + DeviceEventEmitter.removeEventListener('FayeGitter:onDisconnected') + DeviceEventEmitter.removeEventListener('FayeGitter:onFailedToCreate') + DeviceEventEmitter.removeEventListener('FayeGitter:Message') + DeviceEventEmitter.removeEventListener('FayeGitter:log') + DeviceEventEmitter.removeEventListener('FayeGitter:SubscribtionFailed') + DeviceEventEmitter.removeEventListener('FayeGitter:Subscribed') + DeviceEventEmitter.removeEventListener('FayeGitter:Unsubscribed') + } +} + +/** + * Function which parse incoming events and dispatchs needed action + */ + +function parseEvent(event) { + return (dispatch, getState) => { + console.log('MESSAGE', event) + const message = JSON.parse(event.json) + + const {id} = getState().viewer.user + const {activeRoom} = getState().rooms + const roomsChannel = `/api/v1/user/${id}/rooms` + const chatMessages = `/api/v1/rooms/${activeRoom}/chatMessages` + + if (event.channel.match(roomsChannel)) { + dispatch(updateRoomState(message)) + } + + if (event.channel.match(chatMessages)) { + if (!!message.model.fromUser && message.model.fromUser.id !== id && message.operation === 'create') { + dispatch(appendMessages(activeRoom, [message.model])) + } + + if (message.operation === 'update' || message.operation === 'patch') { + dispatch(updateMessageRealtime(activeRoom, message.model)) + } + } + } +} + +export function parseSnapshotEvent(event) { + return dispatch => { + if (!event.log.match('Received message: ')) { + return + } + + const message = JSON.parse(event.log.split('Received message: ')[1]) + + if (message.channel !== '/meta/subscribe' || message.successful !== true) { + return + } + + if (message.hasOwnProperty('ext') && message.ext.hasOwnProperty('snapshot')) { + dispatch(parseSnapshotForChannel(message.subscription, message.ext.snapshot)) + } + } +} + +export function parseSnapshotForChannel(channel, snapshot) { + return dispatch => { + if (channel.match(roomsRegx)) { + dispatch(receiveRoomsSnapshot(snapshot)) + } + + if (channel.match(messagesRegx)) { + const id = channel.match(messagesRegxIds)[1] + dispatch(receiveRoomMessagesSnapshot(id, snapshot)) + } + + if (channel.match(eventsRegx)) { + const id = channel.match(eventsRegxIds)[1] + dispatch(receiveRoomEventsSnapshot(id, snapshot)) + } + + if (channel.match(readByRegx)) { + const messageId = channel.match(readByRegxIds)[2] + dispatch(receiveReadBySnapshot(messageId, snapshot)) + } + } +} + +/** + * Subscribe current user rooms changes (Drawer) + */ + +export function subscribeToRooms() { + return async (dispatch, getState) => { + await checkFayeConnection() + const {id} = getState().viewer.user + const subscription = `/api/v1/user/${id}/rooms` + FayeGitter.subscribe(subscription) + dispatch({type: ROOMS_SUBSCRIBED}) + dispatch(pushSubscription(subscription)) + } +} + +/** + * Subscribe for new room's messages => faye chat messages endpoint + */ + +export function subscribeToChatMessages(roomId) { + return async dispatch => { + await checkFayeConnection() + const subscription = `/api/v1/rooms/${roomId}/chatMessages` + FayeGitter.subscribe(subscription) + dispatch({type: SUBSCRIBE_TO_CHAT_MESSAGES, roomId}) + dispatch(pushSubscription(subscription)) + } +} + +/** + * Unsubscribe for new room's messages => faye chat messages endpoint + */ + +export function unsubscribeToChatMessages(roomId) { + return async (dispatch) => { + await checkFayeConnection() + const subscription = `/api/v1/rooms/${roomId}/chatMessages` + FayeGitter.unsubscribe(subscription) + dispatch({type: UNSUBSCRIBE_TO_CHAT_MESSAGES, roomId}) + dispatch(deleteSubscription(subscription)) + } +} + + +export function subscribeToRoomEvents(roomId) { + return async dispatch => { + await checkFayeConnection() + const subscription = `/api/v1/rooms/${roomId}/events` + FayeGitter.subscribe(subscription) + dispatch({type: SUBSCRIBE_TO_ROOM_EVENTS, roomId}) + dispatch(pushSubscription(subscription)) + } +} + +/** + * Unsubscribe for new room's messages => faye chat messages endpoint + */ + +export function unsubscribeToRoomEvents(roomId) { + return async (dispatch) => { + await checkFayeConnection() + const subscription = `/api/v1/rooms/${roomId}/events` + FayeGitter.unsubscribe(subscription) + dispatch({type: UNSUBSCRIBE_TO_ROOM_EVENTS, roomId}) + dispatch(deleteSubscription(subscription)) + } +} + +export function subscribeToReadBy(roomId, messageId) { + return async dispatch => { + await checkFayeConnection() + const subscription = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` + FayeGitter.subscribe(subscription) + dispatch({type: SUBSCRIBE_TO_READ_BY, roomId}) + dispatch(pushSubscription(subscription)) + } +} + +/** + * Unsubscribe for new room's messages => faye chat messages endpoint + */ + +export function unsubscribeFromReadBy(roomId, messageId) { + return async (dispatch) => { + await checkFayeConnection() + const subscription = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` + FayeGitter.unsubscribe(subscription) + dispatch({type: UNSUBSCRIBE_FROM_READ_BY, roomId}) + dispatch(deleteSubscription(subscription)) + } +} + +export function pushSubscription(subscription) { + return (dispatch, getState) => { + const {subscriptions} = getState().realtime + if (!subscriptions.find(item => item === subscription)) { + dispatch({type: PUSH_SUBSCRIPTION, subscription}) + console.log('PUSH_SUBSCRIPTION', subscription) + } + } +} + +export function deleteSubscription(subscription) { + return (dispatch, getState) => { + const {subscriptions} = getState().realtime + if (!!subscriptions.find(item => item === subscription)) { + dispatch({type: DELETE_SUBSCRIPTION, subscription}) + console.log('DELETE_SUBSCRIPTION', subscription) + } + } +} + +export function subscribeToChannels() { + return (dispatch, getState) => { + const {subscriptions} = getState().realtime + if (subscriptions.length === 0) { + dispatch(subscribeToRooms()) + } else { + subscriptions.forEach(subscription => FayeGitter.subscribe(subscription)) + dispatch({type: SUBSCRIBED_TO_CHANNELS, subscriptions}) + } + } +} + +/** + * Reducer + */ + +const initialState = { + isFayeConnecting: false, + fayeConnected: false, + roomsSubscribed: false, + roomMessagesSubscription: '', + subscriptions: [] +} + +export default function realtime(state = initialState, action) { + switch (action.type) { + case FAYE_CONNECTING: + return {...state, + isFayeConnecting: true + } + case FAYE_CONNECT: + return {...state, + fayeConnected: action.payload, + isFayeConnecting: false + } + + case ROOMS_SUBSCRIBED: { + return {...state, + roomsSubscribed: true + } + } + + case SUBSCRIBE_TO_CHAT_MESSAGES: + return {...state, + roomMessagesSubscription: action.payload + } + + case PUSH_SUBSCRIPTION: + return {...state, + subscriptions: state.subscriptions.concat(action.subscription) + } + + case DELETE_SUBSCRIPTION: + return {...state, + subscriptions: state.subscriptions.filter(subscription => action.subscription !== subscription) + } + + default: + return state + } +} diff --git a/app/modules/realtime.js b/app/modules/realtime.js index f7bc498..edb6aa7 100644 --- a/app/modules/realtime.js +++ b/app/modules/realtime.js @@ -4,6 +4,9 @@ import {updateRoomState, receiveRoomsSnapshot} from './rooms' import {appendMessages, updateMessageRealtime, receiveRoomMessagesSnapshot} from './messages' import {receiveRoomEventsSnapshot} from './activity' import {receiveReadBySnapshot} from './readBy' +import HalleyClient from '../api/faye' + +let client = null /** * Constants @@ -19,9 +22,6 @@ export const SUBSCRIBE_TO_ROOM_EVENTS = 'realtime/SUBSCRIBE_TO_ROOM_EVENTS' export const UNSUBSCRIBE_TO_ROOM_EVENTS = 'realtime/UNSUBSCRIBE_TO_ROOM_EVENTS' export const SUBSCRIBE_TO_READ_BY = 'realtime/SUBSCRIBE_TO_READ_BY' export const UNSUBSCRIBE_FROM_READ_BY = 'realtime/UNSUBSCRIBE_FROM_READ_BY' -export const PUSH_SUBSCRIPTION = 'realtime/PUSH_SUBSCRIPTION' -export const DELETE_SUBSCRIPTION = 'realtime/DELETE_SUBSCRIPTION' -export const SUBSCRIBED_TO_CHANNELS = 'realtime/SUBSCRIBED_TO_CHANNELS' const roomsRegx = /\/api\/v1\/user\/[a-f\d]{24}\/rooms/ const messagesRegx = /\/api\/v1\/rooms\/[a-f\d]{24}\/chatMessages/ @@ -37,33 +37,14 @@ const readByRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/chatMessages\/([a-f\d]{2 */ export function setupFaye() { - return async (dispatch, getState) => { - console.log('RECONNECT TO FAYE') - FayeGitter.setAccessToken(getState().auth.token) - FayeGitter.create() - FayeGitter.logger() - try { - dispatch({type: FAYE_CONNECTING}) - const result = await FayeGitter.connect() - dispatch({type: FAYE_CONNECT, payload: result}) - // dispatch(subscribeToChannels()) - } catch (err) { - console.log(err) // eslint-disable-line no-console - } - } -} - -export function checkFayeConnection() { - return async (dispatch, getState) => { - try { - const connectionStatus = await FayeGitter.checkConnectionStatus() - console.log('CONNECTION_STATUS', connectionStatus) - if (!connectionStatus) { - await dispatch(setupFaye()) - } - } catch (error) { - console.error(error.message) - } + return (dispatch, getState) => { + console.log('CONNECT TO FAYE') + client = new HalleyClient({ + token: getState().auth.token, + snapshotHandler: (message) => dispatch(dispatchSnapshotEvent(message)) + }) + client.setup() + client.create() } } @@ -72,160 +53,58 @@ export function checkFayeConnection() { */ export function onNetStatusChangeFaye(status) { - return async (dispatch, getState) => { - const {isFayeConnecting} = getState().app - if (isFayeConnecting) { - return - } - const connectionStatus = await FayeGitter.checkConnectionStatus() - if (!status && connectionStatus) { - dispatch({type: FAYE_CONNECT, payload: status}) - } - try { - if (status && !connectionStatus) { - await dispatch(setupFaye()) - } - } catch (error) { - console.warn(error.message) - } - } -} - -/** - * Setup faye events - */ - -export function setupFayeEvents() { return (dispatch, getState) => { - const EventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(FayeGitter) : DeviceEventEmitter - EventEmitter - .addListener('FayeGitter:Connected', log => { - console.log('CONNECTED') - dispatch(subscribeToChannels()) - }) - EventEmitter - .addListener('FayeGitter:onDisconnected', log => { - console.log(log) // eslint-disable-line no-console - dispatch(setupFaye()) - }) - EventEmitter - .addListener('FayeGitter:onFailedToCreate', log => { - console.log(log) // eslint-disable-line no-console - const {online} = getState().app - if (online === true) { - dispatch(setupFaye()) - } - }) - EventEmitter - .addListener('FayeGitter:Message', event => { - dispatch(parseEvent(event)) - }) - EventEmitter - .addListener('FayeGitter:log', event => { - dispatch(parseSnapshotEvent(event)) - }) - EventEmitter - .addListener('FayeGitter:SubscribtionFailed', log => console.log(log)) // eslint-disable-line no-console - EventEmitter - .addListener('FayeGitter:Subscribed', event => { - console.log('SUBSCRIBED', event) // eslint-disable-line no-console - if (Platform.OS === 'ios') { - try { - const {channel, ext} = event - const snapshot = JSON.parse(ext).snapshot - if (typeof channel !== 'undefined' && typeof snapshot !== 'undefined') { - dispatch(parseSnapshotForChannel(channel, snapshot)) - } - } catch (error) { - console.warn(error.message) - } - } - }) - EventEmitter - .addListener('FayeGitter:Unsubscribed', log => console.log('UNSUBSCRIBED', log)) // eslint-disable-line no-console } } -export function removeFayeEvents() { - return (dispatch) => { - DeviceEventEmitter.removeEventListener('FayeGitter:Connected') - DeviceEventEmitter.removeEventListener('FayeGitter:onDisconnected') - DeviceEventEmitter.removeEventListener('FayeGitter:onFailedToCreate') - DeviceEventEmitter.removeEventListener('FayeGitter:Message') - DeviceEventEmitter.removeEventListener('FayeGitter:log') - DeviceEventEmitter.removeEventListener('FayeGitter:SubscribtionFailed') - DeviceEventEmitter.removeEventListener('FayeGitter:Subscribed') - DeviceEventEmitter.removeEventListener('FayeGitter:Unsubscribed') - } -} /** * Function which parse incoming events and dispatchs needed action */ -function parseEvent(event) { +function dispatchMessageEvent(message) { return (dispatch, getState) => { - console.log('MESSAGE', event) - const message = JSON.parse(event.json) + console.log('MESSAGE', message) const {id} = getState().viewer.user const {activeRoom} = getState().rooms - const roomsChannel = `/api/v1/user/${id}/rooms` - const chatMessages = `/api/v1/rooms/${activeRoom}/chatMessages` - if (event.channel.match(roomsChannel)) { - dispatch(updateRoomState(message)) + if (!!message.model.fromUser && message.model.fromUser.id !== id && message.operation === 'create') { + dispatch(appendMessages(activeRoom, [message.model])) } - if (event.channel.match(chatMessages)) { - if (!!message.model.fromUser && message.model.fromUser.id !== id && message.operation === 'create') { - dispatch(appendMessages(activeRoom, [message.model])) - } - - if (message.operation === 'update' || message.operation === 'patch') { - dispatch(updateMessageRealtime(activeRoom, message.model)) - } + if (message.operation === 'update' || message.operation === 'patch') { + dispatch(updateMessageRealtime(activeRoom, message.model)) } } } -export function parseSnapshotEvent(event) { - return dispatch => { - if (!event.log.match('Received message: ')) { - return - } - - const message = JSON.parse(event.log.split('Received message: ')[1]) - - if (message.channel !== '/meta/subscribe' || message.successful !== true) { - return - } - - if (message.hasOwnProperty('ext') && message.ext.hasOwnProperty('snapshot')) { - dispatch(parseSnapshotForChannel(message.subscription, message.ext.snapshot)) - } +function dispatchRoomEvent(message) { + return (dispatch, getState) => { + console.log('MESSAGE', message) + dispatch(updateRoomState(message)) } } -export function parseSnapshotForChannel(channel, snapshot) { +export function dispatchSnapshotEvent({subscription, ext: {snapshot}}) { return dispatch => { - if (channel.match(roomsRegx)) { + if (subscription.match(roomsRegx)) { dispatch(receiveRoomsSnapshot(snapshot)) } - if (channel.match(messagesRegx)) { - const id = channel.match(messagesRegxIds)[1] + if (subscription.match(messagesRegx)) { + const id = subscription.match(messagesRegxIds)[1] dispatch(receiveRoomMessagesSnapshot(id, snapshot)) } - if (channel.match(eventsRegx)) { - const id = channel.match(eventsRegxIds)[1] + if (subscription.match(eventsRegx)) { + const id = subscription.match(eventsRegxIds)[1] dispatch(receiveRoomEventsSnapshot(id, snapshot)) } - if (channel.match(readByRegx)) { - const messageId = channel.match(readByRegxIds)[2] + if (subscription.match(readByRegx)) { + const messageId = subscription.match(readByRegxIds)[2] dispatch(receiveReadBySnapshot(messageId, snapshot)) } } @@ -237,12 +116,19 @@ export function parseSnapshotForChannel(channel, snapshot) { export function subscribeToRooms() { return async (dispatch, getState) => { - await checkFayeConnection() - const {id} = getState().viewer.user - const subscription = `/api/v1/user/${id}/rooms` - FayeGitter.subscribe(subscription) - dispatch({type: ROOMS_SUBSCRIBED}) - dispatch(pushSubscription(subscription)) + try { + const {id} = getState().viewer.user + const url = `/api/v1/user/${id}/rooms` + const type = 'userRooms' + const result = await client.subscribe({ + url, + type, + handler: evt => dispatch(dispatchRoomEvent(evt)) + }) + dispatch({type: ROOMS_SUBSCRIBED}) + } catch (err) { + console.log(err) + } } } @@ -252,11 +138,18 @@ export function subscribeToRooms() { export function subscribeToChatMessages(roomId) { return async dispatch => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages` - FayeGitter.subscribe(subscription) - dispatch({type: SUBSCRIBE_TO_CHAT_MESSAGES, roomId}) - dispatch(pushSubscription(subscription)) + try { + const url = `/api/v1/rooms/${roomId}/chatMessages` + const type = 'chatMessages' + const result = await client.subscribe({ + url, + type, + handler: evt => dispatch(dispatchMessageEvent(evt)) + }) + dispatch({type: SUBSCRIBE_TO_CHAT_MESSAGES, roomId}) + } catch (err) { + console.log(err) + } } } @@ -265,23 +158,35 @@ export function subscribeToChatMessages(roomId) { */ export function unsubscribeToChatMessages(roomId) { - return async (dispatch) => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages` - FayeGitter.unsubscribe(subscription) - dispatch({type: UNSUBSCRIBE_TO_CHAT_MESSAGES, roomId}) - dispatch(deleteSubscription(subscription)) + return async dispatch => { + try { + const url = `/api/v1/rooms/${roomId}/chatMessages` + const type = 'chatMessages' + const result = await client.unsubscribe({ + url, + type + }) + dispatch({type: UNSUBSCRIBE_TO_CHAT_MESSAGES, roomId}) + } catch (err) { + console.log(err) + } } } export function subscribeToRoomEvents(roomId) { return async dispatch => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/events` - FayeGitter.subscribe(subscription) - dispatch({type: SUBSCRIBE_TO_ROOM_EVENTS, roomId}) - dispatch(pushSubscription(subscription)) + try { + const url = `/api/v1/rooms/${roomId}/events` + const type = 'roomEvents' + const result = await client.subscribe({ + url, + type + }) + dispatch({type: SUBSCRIBE_TO_ROOM_EVENTS, roomId}) + } catch (err) { + console.log(err) + } } } @@ -291,21 +196,33 @@ export function subscribeToRoomEvents(roomId) { export function unsubscribeToRoomEvents(roomId) { return async (dispatch) => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/events` - FayeGitter.unsubscribe(subscription) - dispatch({type: UNSUBSCRIBE_TO_ROOM_EVENTS, roomId}) - dispatch(deleteSubscription(subscription)) + try { + const url = `/api/v1/rooms/${roomId}/events` + const type = 'roomEvents' + const result = await client.unsubscribe({ + url, + type + }) + dispatch({type: UNSUBSCRIBE_TO_ROOM_EVENTS, roomId}) + } catch (err) { + console.log(err) + } } } export function subscribeToReadBy(roomId, messageId) { return async dispatch => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` - FayeGitter.subscribe(subscription) - dispatch({type: SUBSCRIBE_TO_READ_BY, roomId}) - dispatch(pushSubscription(subscription)) + try { + const url = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` + const type = 'roomEvents' + const result = await client.subscribe({ + url, + type + }) + dispatch({type: SUBSCRIBE_TO_READ_BY, roomId}) + } catch (err) { + console.log(err) + } } } @@ -315,42 +232,16 @@ export function subscribeToReadBy(roomId, messageId) { export function unsubscribeFromReadBy(roomId, messageId) { return async (dispatch) => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` - FayeGitter.unsubscribe(subscription) - dispatch({type: UNSUBSCRIBE_FROM_READ_BY, roomId}) - dispatch(deleteSubscription(subscription)) - } -} - -export function pushSubscription(subscription) { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (!subscriptions.find(item => item === subscription)) { - dispatch({type: PUSH_SUBSCRIPTION, subscription}) - console.log('PUSH_SUBSCRIPTION', subscription) - } - } -} - -export function deleteSubscription(subscription) { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (!!subscriptions.find(item => item === subscription)) { - dispatch({type: DELETE_SUBSCRIPTION, subscription}) - console.log('DELETE_SUBSCRIPTION', subscription) - } - } -} - -export function subscribeToChannels() { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (subscriptions.length === 0) { - dispatch(subscribeToRooms()) - } else { - subscriptions.forEach(subscription => FayeGitter.subscribe(subscription)) - dispatch({type: SUBSCRIBED_TO_CHANNELS, subscriptions}) + try { + const url = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` + const type = 'roomEvents' + const result = await client.unsubscribe({ + url, + type + }) + dispatch({type: UNSUBSCRIBE_FROM_READ_BY, roomId}) + } catch (err) { + console.log(err) } } } @@ -390,16 +281,6 @@ export default function realtime(state = initialState, action) { roomMessagesSubscription: action.payload } - case PUSH_SUBSCRIPTION: - return {...state, - subscriptions: state.subscriptions.concat(action.subscription) - } - - case DELETE_SUBSCRIPTION: - return {...state, - subscriptions: state.subscriptions.filter(subscription => action.subscription !== subscription) - } - default: return state } diff --git a/app/modules/rooms.js b/app/modules/rooms.js index e4a2178..3bba9b1 100644 --- a/app/modules/rooms.js +++ b/app/modules/rooms.js @@ -306,7 +306,7 @@ export function changeNotificationSettings(roomId, index) { export function refreshRooms() { return async (dispatch, getState) => { dispatch(getRooms()) - await checkFayeConnection() + // await checkFayeConnection() } } diff --git a/package.json b/package.json index 93393fa..1021aee 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "android-release": "node node_modules/react-native/local-cli/cli.js run-android --variant=release" }, "dependencies": { + "faye": "^1.2.4", "lodash": "^4.2.1", "moment": "^2.11.2", "react": "16.0.0-alpha.6", @@ -42,14 +43,15 @@ "remote-redux-devtools": "^0.1.1" }, "devDependencies": { + "babel-eslint": "~4.1.6", "babel-jest": "19.0.0", "babel-preset-react-native": "1.9.1", - "jest": "19.0.2", - "react-test-renderer": "16.0.0-alpha.6", "eslint": "~1.10.3", "eslint-config-airbnb": "~2.1.1", "eslint-plugin-react": "~3.13.1", - "babel-eslint": "~4.1.6" + "faye": "^1.2.4", + "jest": "19.0.2", + "react-test-renderer": "16.0.0-alpha.6" }, "jest": { "preset": "react-native" From 94e3926e23a8d4117274fc8f0735a2e415c64c8e Mon Sep 17 00:00:00 2001 From: Terry Sahaidak Date: Mon, 1 May 2017 00:04:33 +0300 Subject: [PATCH 2/5] Fix toolbar icons sizes --- app/modules/old.js | 406 ------------------------------------------ app/utils/iconsMap.js | 3 +- 2 files changed, 2 insertions(+), 407 deletions(-) delete mode 100644 app/modules/old.js diff --git a/app/modules/old.js b/app/modules/old.js deleted file mode 100644 index f7bc498..0000000 --- a/app/modules/old.js +++ /dev/null @@ -1,406 +0,0 @@ -import FayeGitter from '../../libs/react-native-gitter-faye/index' -import {DeviceEventEmitter, NativeEventEmitter, Platform} from 'react-native' -import {updateRoomState, receiveRoomsSnapshot} from './rooms' -import {appendMessages, updateMessageRealtime, receiveRoomMessagesSnapshot} from './messages' -import {receiveRoomEventsSnapshot} from './activity' -import {receiveReadBySnapshot} from './readBy' - -/** - * Constants - */ - -export const FAYE_CONNECTING = 'realtime/FAYE_CONNECTING' -export const FAYE_CONNECT = 'realtime/FAYE_CONNECT' -export const ROOMS_SUBSCRIBED = 'realtime/ROOMS_SUBSCRIBED' -export const ROOMS_UNSUBSCRIBED = 'realtime/ROOMS_UNSUBSCRIBED' -export const SUBSCRIBE_TO_CHAT_MESSAGES = 'realtime/SUBSCRIBE_TO_CHAT_MESSAGES' -export const UNSUBSCRIBE_TO_CHAT_MESSAGES = 'realtime/UNSUBSCRIBE_TO_CHAT_MESSAGES' -export const SUBSCRIBE_TO_ROOM_EVENTS = 'realtime/SUBSCRIBE_TO_ROOM_EVENTS' -export const UNSUBSCRIBE_TO_ROOM_EVENTS = 'realtime/UNSUBSCRIBE_TO_ROOM_EVENTS' -export const SUBSCRIBE_TO_READ_BY = 'realtime/SUBSCRIBE_TO_READ_BY' -export const UNSUBSCRIBE_FROM_READ_BY = 'realtime/UNSUBSCRIBE_FROM_READ_BY' -export const PUSH_SUBSCRIPTION = 'realtime/PUSH_SUBSCRIPTION' -export const DELETE_SUBSCRIPTION = 'realtime/DELETE_SUBSCRIPTION' -export const SUBSCRIBED_TO_CHANNELS = 'realtime/SUBSCRIBED_TO_CHANNELS' - -const roomsRegx = /\/api\/v1\/user\/[a-f\d]{24}\/rooms/ -const messagesRegx = /\/api\/v1\/rooms\/[a-f\d]{24}\/chatMessages/ -const messagesRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/chatMessages/ -const eventsRegx = /\/api\/v1\/rooms\/[a-f\d]{24}\/events/ -const eventsRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/events/ -const readByRegx = /\/api\/v1\/rooms\/[a-f\d]{24}\/chatMessages\/[a-f\d]{24}\/readBy/ -const readByRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/chatMessages\/([a-f\d]{24})\/readBy/ - - -/** - * Actions - */ - -export function setupFaye() { - return async (dispatch, getState) => { - console.log('RECONNECT TO FAYE') - FayeGitter.setAccessToken(getState().auth.token) - FayeGitter.create() - FayeGitter.logger() - try { - dispatch({type: FAYE_CONNECTING}) - const result = await FayeGitter.connect() - dispatch({type: FAYE_CONNECT, payload: result}) - // dispatch(subscribeToChannels()) - } catch (err) { - console.log(err) // eslint-disable-line no-console - } - } -} - -export function checkFayeConnection() { - return async (dispatch, getState) => { - try { - const connectionStatus = await FayeGitter.checkConnectionStatus() - console.log('CONNECTION_STATUS', connectionStatus) - if (!connectionStatus) { - await dispatch(setupFaye()) - } - } catch (error) { - console.error(error.message) - } - } -} - -/** - * Handler which handles net status changes and reconnects to faye if needed - */ - -export function onNetStatusChangeFaye(status) { - return async (dispatch, getState) => { - const {isFayeConnecting} = getState().app - if (isFayeConnecting) { - return - } - const connectionStatus = await FayeGitter.checkConnectionStatus() - if (!status && connectionStatus) { - dispatch({type: FAYE_CONNECT, payload: status}) - } - try { - if (status && !connectionStatus) { - await dispatch(setupFaye()) - } - } catch (error) { - console.warn(error.message) - } - } -} - -/** - * Setup faye events - */ - -export function setupFayeEvents() { - return (dispatch, getState) => { - const EventEmitter = Platform.OS === 'ios' ? new NativeEventEmitter(FayeGitter) : DeviceEventEmitter - EventEmitter - .addListener('FayeGitter:Connected', log => { - console.log('CONNECTED') - dispatch(subscribeToChannels()) - }) - EventEmitter - .addListener('FayeGitter:onDisconnected', log => { - console.log(log) // eslint-disable-line no-console - dispatch(setupFaye()) - }) - - EventEmitter - .addListener('FayeGitter:onFailedToCreate', log => { - console.log(log) // eslint-disable-line no-console - const {online} = getState().app - if (online === true) { - dispatch(setupFaye()) - } - }) - EventEmitter - .addListener('FayeGitter:Message', event => { - dispatch(parseEvent(event)) - }) - EventEmitter - .addListener('FayeGitter:log', event => { - dispatch(parseSnapshotEvent(event)) - }) - EventEmitter - .addListener('FayeGitter:SubscribtionFailed', log => console.log(log)) // eslint-disable-line no-console - EventEmitter - .addListener('FayeGitter:Subscribed', event => { - console.log('SUBSCRIBED', event) // eslint-disable-line no-console - if (Platform.OS === 'ios') { - try { - const {channel, ext} = event - const snapshot = JSON.parse(ext).snapshot - if (typeof channel !== 'undefined' && typeof snapshot !== 'undefined') { - dispatch(parseSnapshotForChannel(channel, snapshot)) - } - } catch (error) { - console.warn(error.message) - } - } - }) - EventEmitter - .addListener('FayeGitter:Unsubscribed', log => console.log('UNSUBSCRIBED', log)) // eslint-disable-line no-console - } -} - -export function removeFayeEvents() { - return (dispatch) => { - DeviceEventEmitter.removeEventListener('FayeGitter:Connected') - DeviceEventEmitter.removeEventListener('FayeGitter:onDisconnected') - DeviceEventEmitter.removeEventListener('FayeGitter:onFailedToCreate') - DeviceEventEmitter.removeEventListener('FayeGitter:Message') - DeviceEventEmitter.removeEventListener('FayeGitter:log') - DeviceEventEmitter.removeEventListener('FayeGitter:SubscribtionFailed') - DeviceEventEmitter.removeEventListener('FayeGitter:Subscribed') - DeviceEventEmitter.removeEventListener('FayeGitter:Unsubscribed') - } -} - -/** - * Function which parse incoming events and dispatchs needed action - */ - -function parseEvent(event) { - return (dispatch, getState) => { - console.log('MESSAGE', event) - const message = JSON.parse(event.json) - - const {id} = getState().viewer.user - const {activeRoom} = getState().rooms - const roomsChannel = `/api/v1/user/${id}/rooms` - const chatMessages = `/api/v1/rooms/${activeRoom}/chatMessages` - - if (event.channel.match(roomsChannel)) { - dispatch(updateRoomState(message)) - } - - if (event.channel.match(chatMessages)) { - if (!!message.model.fromUser && message.model.fromUser.id !== id && message.operation === 'create') { - dispatch(appendMessages(activeRoom, [message.model])) - } - - if (message.operation === 'update' || message.operation === 'patch') { - dispatch(updateMessageRealtime(activeRoom, message.model)) - } - } - } -} - -export function parseSnapshotEvent(event) { - return dispatch => { - if (!event.log.match('Received message: ')) { - return - } - - const message = JSON.parse(event.log.split('Received message: ')[1]) - - if (message.channel !== '/meta/subscribe' || message.successful !== true) { - return - } - - if (message.hasOwnProperty('ext') && message.ext.hasOwnProperty('snapshot')) { - dispatch(parseSnapshotForChannel(message.subscription, message.ext.snapshot)) - } - } -} - -export function parseSnapshotForChannel(channel, snapshot) { - return dispatch => { - if (channel.match(roomsRegx)) { - dispatch(receiveRoomsSnapshot(snapshot)) - } - - if (channel.match(messagesRegx)) { - const id = channel.match(messagesRegxIds)[1] - dispatch(receiveRoomMessagesSnapshot(id, snapshot)) - } - - if (channel.match(eventsRegx)) { - const id = channel.match(eventsRegxIds)[1] - dispatch(receiveRoomEventsSnapshot(id, snapshot)) - } - - if (channel.match(readByRegx)) { - const messageId = channel.match(readByRegxIds)[2] - dispatch(receiveReadBySnapshot(messageId, snapshot)) - } - } -} - -/** - * Subscribe current user rooms changes (Drawer) - */ - -export function subscribeToRooms() { - return async (dispatch, getState) => { - await checkFayeConnection() - const {id} = getState().viewer.user - const subscription = `/api/v1/user/${id}/rooms` - FayeGitter.subscribe(subscription) - dispatch({type: ROOMS_SUBSCRIBED}) - dispatch(pushSubscription(subscription)) - } -} - -/** - * Subscribe for new room's messages => faye chat messages endpoint - */ - -export function subscribeToChatMessages(roomId) { - return async dispatch => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages` - FayeGitter.subscribe(subscription) - dispatch({type: SUBSCRIBE_TO_CHAT_MESSAGES, roomId}) - dispatch(pushSubscription(subscription)) - } -} - -/** - * Unsubscribe for new room's messages => faye chat messages endpoint - */ - -export function unsubscribeToChatMessages(roomId) { - return async (dispatch) => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages` - FayeGitter.unsubscribe(subscription) - dispatch({type: UNSUBSCRIBE_TO_CHAT_MESSAGES, roomId}) - dispatch(deleteSubscription(subscription)) - } -} - - -export function subscribeToRoomEvents(roomId) { - return async dispatch => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/events` - FayeGitter.subscribe(subscription) - dispatch({type: SUBSCRIBE_TO_ROOM_EVENTS, roomId}) - dispatch(pushSubscription(subscription)) - } -} - -/** - * Unsubscribe for new room's messages => faye chat messages endpoint - */ - -export function unsubscribeToRoomEvents(roomId) { - return async (dispatch) => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/events` - FayeGitter.unsubscribe(subscription) - dispatch({type: UNSUBSCRIBE_TO_ROOM_EVENTS, roomId}) - dispatch(deleteSubscription(subscription)) - } -} - -export function subscribeToReadBy(roomId, messageId) { - return async dispatch => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` - FayeGitter.subscribe(subscription) - dispatch({type: SUBSCRIBE_TO_READ_BY, roomId}) - dispatch(pushSubscription(subscription)) - } -} - -/** - * Unsubscribe for new room's messages => faye chat messages endpoint - */ - -export function unsubscribeFromReadBy(roomId, messageId) { - return async (dispatch) => { - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` - FayeGitter.unsubscribe(subscription) - dispatch({type: UNSUBSCRIBE_FROM_READ_BY, roomId}) - dispatch(deleteSubscription(subscription)) - } -} - -export function pushSubscription(subscription) { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (!subscriptions.find(item => item === subscription)) { - dispatch({type: PUSH_SUBSCRIPTION, subscription}) - console.log('PUSH_SUBSCRIPTION', subscription) - } - } -} - -export function deleteSubscription(subscription) { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (!!subscriptions.find(item => item === subscription)) { - dispatch({type: DELETE_SUBSCRIPTION, subscription}) - console.log('DELETE_SUBSCRIPTION', subscription) - } - } -} - -export function subscribeToChannels() { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (subscriptions.length === 0) { - dispatch(subscribeToRooms()) - } else { - subscriptions.forEach(subscription => FayeGitter.subscribe(subscription)) - dispatch({type: SUBSCRIBED_TO_CHANNELS, subscriptions}) - } - } -} - -/** - * Reducer - */ - -const initialState = { - isFayeConnecting: false, - fayeConnected: false, - roomsSubscribed: false, - roomMessagesSubscription: '', - subscriptions: [] -} - -export default function realtime(state = initialState, action) { - switch (action.type) { - case FAYE_CONNECTING: - return {...state, - isFayeConnecting: true - } - case FAYE_CONNECT: - return {...state, - fayeConnected: action.payload, - isFayeConnecting: false - } - - case ROOMS_SUBSCRIBED: { - return {...state, - roomsSubscribed: true - } - } - - case SUBSCRIBE_TO_CHAT_MESSAGES: - return {...state, - roomMessagesSubscription: action.payload - } - - case PUSH_SUBSCRIPTION: - return {...state, - subscriptions: state.subscriptions.concat(action.subscription) - } - - case DELETE_SUBSCRIPTION: - return {...state, - subscriptions: state.subscriptions.filter(subscription => action.subscription !== subscription) - } - - default: - return state - } -} diff --git a/app/utils/iconsMap.js b/app/utils/iconsMap.js index 0d39163..d343ced 100644 --- a/app/utils/iconsMap.js +++ b/app/utils/iconsMap.js @@ -1,5 +1,6 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons' import {icons} from '../constants' +import {PixelRatio, Platform} from 'react-native' export const iconsMap = {}; export const iconsLoaded = new Promise((resolve, reject) => { @@ -9,7 +10,7 @@ export const iconsLoaded = new Promise((resolve, reject) => { const Provider = MaterialIcons return Provider.getImageSource( icon.icon, - icon.size, + Platform.OS === 'ios' ? icon.size : PixelRatio.getPixelSizeForLayoutSize(icon.size), icon.color ) }) From a4ad3a1e7a6bfa3b48260b5eef6f0a861e361197 Mon Sep 17 00:00:00 2001 From: Terry Sahaidak Date: Fri, 26 May 2017 12:44:41 +0300 Subject: [PATCH 3/5] Try faye web --- app/api/faye.js | 47 +- app/modules/app.js | 1 + app/modules/realtime.js | 24 +- app/screens/RoomSettings/index.js | 4 - libs/halley/.ackrc | 4 + libs/halley/.eslintrc.json | 21 + libs/halley/.jshintrc | 4 + libs/halley/.npmignore | 8 + libs/halley/.travis.yml | 9 + libs/halley/README.md | 171 +++++ libs/halley/backbone/browser.js | 19 + libs/halley/backbone/node.js | 19 + libs/halley/backbone/package.json | 6 + libs/halley/browser-standalone.js | 19 + libs/halley/gulpfile.js | 183 +++++ libs/halley/index.js | 18 + libs/halley/karma.conf.js | 134 ++++ libs/halley/lib/main.js | 8 + libs/halley/lib/mixins/statemachine-mixin.js | 191 ++++++ libs/halley/lib/protocol/advice.js | 192 ++++++ libs/halley/lib/protocol/channel-set.js | 109 +++ libs/halley/lib/protocol/channel.js | 90 +++ libs/halley/lib/protocol/client.js | 525 +++++++++++++++ libs/halley/lib/protocol/dispatcher.js | 298 +++++++++ libs/halley/lib/protocol/envelope.js | 56 ++ libs/halley/lib/protocol/extensions.js | 51 ++ libs/halley/lib/protocol/scheduler.js | 70 ++ .../halley/lib/protocol/subscribe-thenable.js | 44 ++ libs/halley/lib/protocol/subscription.js | 33 + .../transport/base-long-polling-transport.js | 59 ++ libs/halley/lib/transport/base-websocket.js | 252 +++++++ libs/halley/lib/transport/browser/.jshintrc | 5 + .../transport/browser/browser-websocket.js | 31 + libs/halley/lib/transport/browser/xhr.js | 134 ++++ libs/halley/lib/transport/node/.jshintrc | 5 + libs/halley/lib/transport/node/node-http.js | 109 +++ .../lib/transport/node/node-websocket.js | 24 + libs/halley/lib/transport/pool.js | 185 ++++++ libs/halley/lib/transport/transport.js | 56 ++ libs/halley/lib/util/errors.js | 58 ++ libs/halley/lib/util/externals.js | 11 + libs/halley/lib/util/global-events.js | 61 ++ libs/halley/lib/util/promise-util.js | 303 +++++++++ libs/halley/lib/util/uri.js | 77 +++ libs/halley/package.json | 140 ++++ libs/halley/test/.eslintrc.json | 7 + libs/halley/test/.jshintrc | 12 + libs/halley/test/browser-websocket-test.js | 51 ++ libs/halley/test/channel-set-test.js | 140 ++++ .../halley/test/client-all-transports-test.js | 73 ++ libs/halley/test/client-long-polling-test.js | 61 ++ libs/halley/test/client-shutdown-test.js | 32 + libs/halley/test/client-websockets-test.js | 78 +++ libs/halley/test/errors-test.js | 27 + libs/halley/test/extensions-test.js | 39 ++ libs/halley/test/helpers/bayeux-server.js | 191 ++++++ .../test/helpers/bayeux-with-proxy-server.js | 73 ++ .../test/helpers/cleanup-test-process.js | 49 ++ libs/halley/test/helpers/proxy-server.js | 123 ++++ libs/halley/test/helpers/public/index.html | 13 + .../test/helpers/remote-server-control.js | 83 +++ libs/halley/test/helpers/server.js | 208 ++++++ libs/halley/test/node-websocket-test.js | 52 ++ libs/halley/test/on-before-unload-test.js | 34 + libs/halley/test/promise-util-test.js | 625 ++++++++++++++++++ libs/halley/test/specs/client-advice-spec.js | 104 +++ .../test/specs/client-bad-connection-spec.js | 129 ++++ .../test/specs/client-bad-websockets-spec.js | 47 ++ libs/halley/test/specs/client-connect-spec.js | 57 ++ libs/halley/test/specs/client-delete-spec.js | 57 ++ .../specs/client-invalid-endpoint-spec.js | 25 + libs/halley/test/specs/client-proxied-spec.js | 6 + libs/halley/test/specs/client-publish-spec.js | 78 +++ libs/halley/test/specs/client-reset-spec.js | 64 ++ .../test/specs/client-server-restart-spec.js | 60 ++ libs/halley/test/specs/client-spec.js | 11 + .../test/specs/client-subscribe-spec.js | 248 +++++++ .../specs/websocket-bad-connection-spec.js | 53 ++ .../specs/websocket-server-restart-spec.js | 25 + libs/halley/test/specs/websocket-spec.js | 39 ++ libs/halley/test/statemachine-mixin-test.js | 559 ++++++++++++++++ libs/halley/test/test-suite-browser.js | 63 ++ libs/halley/test/test-suite-node.js | 78 +++ libs/halley/test/transport-pool-test.js | 242 +++++++ npm-debug.log.1906906186 | 0 package.json | 2 +- 86 files changed, 7769 insertions(+), 17 deletions(-) create mode 100644 libs/halley/.ackrc create mode 100644 libs/halley/.eslintrc.json create mode 100644 libs/halley/.jshintrc create mode 100644 libs/halley/.npmignore create mode 100644 libs/halley/.travis.yml create mode 100644 libs/halley/README.md create mode 100644 libs/halley/backbone/browser.js create mode 100644 libs/halley/backbone/node.js create mode 100644 libs/halley/backbone/package.json create mode 100644 libs/halley/browser-standalone.js create mode 100644 libs/halley/gulpfile.js create mode 100644 libs/halley/index.js create mode 100644 libs/halley/karma.conf.js create mode 100644 libs/halley/lib/main.js create mode 100644 libs/halley/lib/mixins/statemachine-mixin.js create mode 100644 libs/halley/lib/protocol/advice.js create mode 100644 libs/halley/lib/protocol/channel-set.js create mode 100644 libs/halley/lib/protocol/channel.js create mode 100644 libs/halley/lib/protocol/client.js create mode 100644 libs/halley/lib/protocol/dispatcher.js create mode 100644 libs/halley/lib/protocol/envelope.js create mode 100644 libs/halley/lib/protocol/extensions.js create mode 100644 libs/halley/lib/protocol/scheduler.js create mode 100644 libs/halley/lib/protocol/subscribe-thenable.js create mode 100644 libs/halley/lib/protocol/subscription.js create mode 100644 libs/halley/lib/transport/base-long-polling-transport.js create mode 100644 libs/halley/lib/transport/base-websocket.js create mode 100644 libs/halley/lib/transport/browser/.jshintrc create mode 100644 libs/halley/lib/transport/browser/browser-websocket.js create mode 100644 libs/halley/lib/transport/browser/xhr.js create mode 100644 libs/halley/lib/transport/node/.jshintrc create mode 100644 libs/halley/lib/transport/node/node-http.js create mode 100644 libs/halley/lib/transport/node/node-websocket.js create mode 100644 libs/halley/lib/transport/pool.js create mode 100644 libs/halley/lib/transport/transport.js create mode 100644 libs/halley/lib/util/errors.js create mode 100644 libs/halley/lib/util/externals.js create mode 100644 libs/halley/lib/util/global-events.js create mode 100644 libs/halley/lib/util/promise-util.js create mode 100644 libs/halley/lib/util/uri.js create mode 100644 libs/halley/package.json create mode 100644 libs/halley/test/.eslintrc.json create mode 100644 libs/halley/test/.jshintrc create mode 100644 libs/halley/test/browser-websocket-test.js create mode 100644 libs/halley/test/channel-set-test.js create mode 100644 libs/halley/test/client-all-transports-test.js create mode 100644 libs/halley/test/client-long-polling-test.js create mode 100644 libs/halley/test/client-shutdown-test.js create mode 100644 libs/halley/test/client-websockets-test.js create mode 100644 libs/halley/test/errors-test.js create mode 100644 libs/halley/test/extensions-test.js create mode 100644 libs/halley/test/helpers/bayeux-server.js create mode 100644 libs/halley/test/helpers/bayeux-with-proxy-server.js create mode 100644 libs/halley/test/helpers/cleanup-test-process.js create mode 100644 libs/halley/test/helpers/proxy-server.js create mode 100644 libs/halley/test/helpers/public/index.html create mode 100644 libs/halley/test/helpers/remote-server-control.js create mode 100644 libs/halley/test/helpers/server.js create mode 100644 libs/halley/test/node-websocket-test.js create mode 100644 libs/halley/test/on-before-unload-test.js create mode 100644 libs/halley/test/promise-util-test.js create mode 100644 libs/halley/test/specs/client-advice-spec.js create mode 100644 libs/halley/test/specs/client-bad-connection-spec.js create mode 100644 libs/halley/test/specs/client-bad-websockets-spec.js create mode 100644 libs/halley/test/specs/client-connect-spec.js create mode 100644 libs/halley/test/specs/client-delete-spec.js create mode 100644 libs/halley/test/specs/client-invalid-endpoint-spec.js create mode 100644 libs/halley/test/specs/client-proxied-spec.js create mode 100644 libs/halley/test/specs/client-publish-spec.js create mode 100644 libs/halley/test/specs/client-reset-spec.js create mode 100644 libs/halley/test/specs/client-server-restart-spec.js create mode 100644 libs/halley/test/specs/client-spec.js create mode 100644 libs/halley/test/specs/client-subscribe-spec.js create mode 100644 libs/halley/test/specs/websocket-bad-connection-spec.js create mode 100644 libs/halley/test/specs/websocket-server-restart-spec.js create mode 100644 libs/halley/test/specs/websocket-spec.js create mode 100644 libs/halley/test/statemachine-mixin-test.js create mode 100644 libs/halley/test/test-suite-browser.js create mode 100644 libs/halley/test/test-suite-node.js create mode 100644 libs/halley/test/transport-pool-test.js create mode 100644 npm-debug.log.1906906186 diff --git a/app/api/faye.js b/app/api/faye.js index a9850a7..b789829 100644 --- a/app/api/faye.js +++ b/app/api/faye.js @@ -1,5 +1,6 @@ -import Faye from 'faye' +import Faye from '../../libs/halley/browser-standalone' const noop = () => {} +import {NetInfo} from 'react-native' class ClientAuthExt { constructor(token) { @@ -9,7 +10,8 @@ class ClientAuthExt { outgoing(message, cb) { if (message.channel === '/meta/handshake') { if (!message.ext) { message.ext = {}; } - message.ext.token = this._token; + message.ext.token = this._token + message.ext.realtimeLibrary = 'halley' } cb(message); @@ -56,14 +58,19 @@ class SnapshotExt { export default class HalleyClient { constructor({token, snapshotHandler}) { - this._client = new Faye.Client('https://ws.gitter.im/faye', { - timeout: 60, - retry: 1, - interval: 0 + this._client = new Faye.Client('https://ws.gitter.im/bayeux', { + timeout: 60000, + retry: 1000, + interval: 1000 }) this._token = token this._snapshotHandler = snapshotHandler this._subsciptions = [] + this._isConnectedToNetwork = true + this._transport = true + + this.setupNetworkListeners() + this.setupInternetListener() } setToken(token) { @@ -132,6 +139,34 @@ export default class HalleyClient { }) } + setupNetworkListeners() { + // this._client.on('transport:down', () => { + // this._transport = false + // }) + // + // this._client.on('transport:up', () => { + // this._transport = true + // }) + } + + setupInternetListener() { + // NetInfo.isConnected.addEventListener( + // 'change', + // () => { + // debugger + // NetInfo.isConnected.fetch() + // .then(isConnected => { + // if (isConnected) { + // this._client.connect() + // // this._isConnectedToNetwork = true + // } else { + // // this._isConnectedToNetwork = false + // } + // }) + // } + // ) + } + _checkSubscriptionAlreadyExist(subscriptionOptions) { const subscription = this._findSubscription(subscriptionOptions) return !!subscription diff --git a/app/modules/app.js b/app/modules/app.js index e7b14dc..89e40d0 100644 --- a/app/modules/app.js +++ b/app/modules/app.js @@ -55,6 +55,7 @@ export function init() { ]) rootNavigator.startAppWithScreen({screen: 'gm.Home', showDrawer: true}) + dispatch(subscribeToRooms()) dispatch(setupNetStatusListener()) // if you need debug room screen, just comment nevigation to 'hone' // and uncomment navigation to 'room' diff --git a/app/modules/realtime.js b/app/modules/realtime.js index edb6aa7..574d967 100644 --- a/app/modules/realtime.js +++ b/app/modules/realtime.js @@ -120,12 +120,18 @@ export function subscribeToRooms() { const {id} = getState().viewer.user const url = `/api/v1/user/${id}/rooms` const type = 'userRooms' - const result = await client.subscribe({ - url, - type, - handler: evt => dispatch(dispatchRoomEvent(evt)) - }) + // const result = await client.subscribe({ + // url, + // type, + // handler: evt => dispatch(dispatchRoomEvent(evt)) + // }) dispatch({type: ROOMS_SUBSCRIBED}) + + // await client.subscribe({ + // url: `/api/v1/user/${id}`, + // type, + // handler: evt => console.log('USER: ', evt) + // }) } catch (err) { console.log(err) } @@ -137,7 +143,7 @@ export function subscribeToRooms() { */ export function subscribeToChatMessages(roomId) { - return async dispatch => { + return async (dispatch, getState) => { try { const url = `/api/v1/rooms/${roomId}/chatMessages` const type = 'chatMessages' @@ -147,6 +153,12 @@ export function subscribeToChatMessages(roomId) { handler: evt => dispatch(dispatchMessageEvent(evt)) }) dispatch({type: SUBSCRIBE_TO_CHAT_MESSAGES, roomId}) + // const {id} = getState().viewer.user + // await client.subscribe({ + // url: `/api/v1/user/${id}/rooms/${roomId}/unreadItems`, + // type: 'ureadItems', + // handler: evt => console.log('UnreadItems: ', evt) + // }) } catch (err) { console.log(err) } diff --git a/app/screens/RoomSettings/index.js b/app/screens/RoomSettings/index.js index 9e08148..2840d0c 100644 --- a/app/screens/RoomSettings/index.js +++ b/app/screens/RoomSettings/index.js @@ -73,10 +73,6 @@ class RoomSettings extends Component { const {dispatch, roomId} = this.props dispatch(changeNotificationSettings(roomId, pickerState)) - - setTimeout(() => { - this.navigateBack() - }, 500) } renderNotificationsSection() { diff --git a/libs/halley/.ackrc b/libs/halley/.ackrc new file mode 100644 index 0000000..2edb143 --- /dev/null +++ b/libs/halley/.ackrc @@ -0,0 +1,4 @@ +--ignore-dir +dist/ +--ignore-dir +node_modules/ diff --git a/libs/halley/.eslintrc.json b/libs/halley/.eslintrc.json new file mode 100644 index 0000000..3ca8209 --- /dev/null +++ b/libs/halley/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "rules": { + "quotes": [ + 2, + "single" + ], + "linebreak-style": [ + 2, + "unix" + ], + "semi": [ + 2, + "always" + ] + }, + "env": { + "node": true, + "browser": true + }, + "extends": "eslint:recommended" +} diff --git a/libs/halley/.jshintrc b/libs/halley/.jshintrc new file mode 100644 index 0000000..0441126 --- /dev/null +++ b/libs/halley/.jshintrc @@ -0,0 +1,4 @@ +{ + "node": true, + "unused": true +} diff --git a/libs/halley/.npmignore b/libs/halley/.npmignore new file mode 100644 index 0000000..9ad5fd1 --- /dev/null +++ b/libs/halley/.npmignore @@ -0,0 +1,8 @@ +*.gem +build +Gemfile.lock +node_modules +.wake.json +coverage +dist +.env diff --git a/libs/halley/.travis.yml b/libs/halley/.travis.yml new file mode 100644 index 0000000..ed95b51 --- /dev/null +++ b/libs/halley/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: +- '0.12' +- '0.10' +- '4.2' +before_script: + - npm install -g gulp +script: gulp +after_success: ./node_modules/.bin/coveralls --verbose < dist/coverage/lcov.info diff --git a/libs/halley/README.md b/libs/halley/README.md new file mode 100644 index 0000000..7ed6067 --- /dev/null +++ b/libs/halley/README.md @@ -0,0 +1,171 @@ +# Halley + +[![Join the chat at https://gitter.im/gitterHQ/halley](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gitterHQ/halley?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/gitterHQ/halley.svg)](https://travis-ci.org/gitterHQ/halley) [![Coverage Status](https://coveralls.io/repos/gitterHQ/halley/badge.svg?branch=master&service=github)](https://coveralls.io/github/gitterHQ/halley?branch=master) + +Halley is an experimental fork of James Coglan's excellent Faye library. + +## Differences from Faye + +The main differences from Faye are (listed in no particular order): +* **Uses promises** (and Bluebird's promise cancellation feature) to do the heavy-lifting whereever possible. +* No Ruby client or server and no server support. Halley is a **Javascript Bayeux client only** +* **Webpack/browserify packaging** +* **Client reset support**. This will force the client to rehandshake. This can be useful when the application realises that the connection is dead before the bayeux client does and allows for faster recovery in these situations. +* **No eventsource support** as we've found them to be unreliable in a ELB/haproxy setup +* All **durations are in milliseconds**, not seconds +* Wherever possible, implementations have been replaced with external libraries: + * Uses [bluebird](https://github.com/petkaantonov/bluebird/) for promises + * Uses backbone events (or backbone-events-standalone) for events + * Mocha/sinon/karma for testing + +## Why's it called "Halley"? + +Lots of reasons! Halley implements the Bayeux Protocol. The [Bayeux Tapestry](https://en.wikipedia.org/wiki/Bayeux_Tapestry) +contains the first know depiction of Halley's Comet. Halley is a [cometd](https://cometd.org) client. + +### Usage + +### Basic Example + +```js +var Halley = require('halley'); +var client = new Halley.Client('/bayeux'); + +function onMessage(message) { + console.log('Incoming message', message); +} + +client.subscribe('/channel', onMessage); + +client.publish('/channel2', { value: 1 }) + .then(function(response) { + console.log('Publish returned', response); + }) + .catch(function(err) { + console.error('Publish failed:', err); + }); +``` + +### Advanced Example + +```js +var Halley = require('halley'); +var Promise = require('bluebird'); + +/** Create a client (showing the default options) */ +var client = new Halley.Client('/bayeux', { + /* The amount of time to wait (in ms) between successive + * retries on a single message send */ + retry: 30000, + + /** + * An integer representing the minimum period of time, in milliseconds, for a + * client to delay subsequent requests to the /meta/connect channel. + * A negative period indicates that the message should not be retried. + * A client MUST implement interval support, but a client MAY exceed the + * interval provided by the server. A client SHOULD implement a backoff + * strategy to increase the interval if requests to the server fail without + * new advice being received from the server. + */ + interval: 0, + + /** + * An integer representing the period of time, in milliseconds, for the + * server to delay responses to the /meta/connect channel. + * This value is merely informative for clients. Bayeux servers SHOULD honor + * timeout advices sent by clients. + */ + timeout: 30000, + + /** + * The maximum number of milliseconds to wait before considering a + * request to the Bayeux server failed. + */ + maxNetworkDelay: 30000, + + /** + * The maximum number of milliseconds to wait for a WebSocket connection to + * be opened. It does not apply to HTTP connections. + */ + connectTimeout: 30000, + + /** + * Maximum time to wait on disconnect + */ + disconnectTimeout: 10000 +}); + +function onMessage(message) { + console.log('Incoming message', message); +} + +/* + *`.subscribe` returns a thenable with a `.unsubscribe` method + * but will also resolve as a promise + */ +var subscription = client.subscribe('/channel', onMessage); + +subscription + .then(function() { + console.log('Subscription successful'); + }) + .catch(function(err) { + console.log('Subscription failed: ', err); + }); + +/** As an example, wait 10 seconds and cancel the subscription */ +Promise.delay(10000) + .then(function() { + return subscription.unsubscribe(); + }); +``` + + +### Debugging + +Halley uses [debug](https://github.com/visionmedia/debug) for debugging. + + * To enable in nodejs, `export DEBUG=halley:*` + * To enable in a browser, `window.localStorage.debug='halley:*'` + +To limit the amount of debug logging produced, you can specify individual categories, eg `export DEBUG=halley:client`. + +## Tests + +Most of the tests in Halley are end-to-end integration tests, which means running a server environment alongside client tests which run in the browser. + +In order to isolate tests from one another, the server will spawn a new Faye server and Proxy server for each test (and tear them down when the test is complete). + +Some of the tests connect to Faye directly, while other tests are performed via the Proxy server which is intended to simulate an reverse-proxy/ELB situation common in many production environments. + +The tests do horrible things in order to test some of the situations we've discovered when using Bayeux and websockets on the web. Examples of things we test to ensure that the client recovers include: + +* Corrupting websocket streams, like bad MITM proxies sometimes do +* Dropping random packets +* Restarting the server during the test +* Deleting the client connection from the server during the test +* Not communicating TCP disconnects from the server-to-client and client-to-server when communicating via the proxy (a situation we've seen on ELB) + +## License + +(The MIT License) + +Copyright (c) 2009-2014 James Coglan and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/halley/backbone/browser.js b/libs/halley/backbone/browser.js new file mode 100644 index 0000000..18a9bee --- /dev/null +++ b/libs/halley/backbone/browser.js @@ -0,0 +1,19 @@ +'use strict'; + +var Promise = require('bluebird'); +Promise.config({ + cancellation: true +}); + +require('../lib/util/externals').use({ + Events: require('backbone').Events, + extend: require('underscore').extend +}); + +var Transport = require('../lib/transport/transport'); + +/* Register the transports. Order is important */ +Transport.register('websocket' , require('../lib/transport/browser/browser-websocket')); +Transport.register('long-polling' , require('../lib/transport/browser/xhr')); + +module.exports = require('../lib/main'); diff --git a/libs/halley/backbone/node.js b/libs/halley/backbone/node.js new file mode 100644 index 0000000..4ad7256 --- /dev/null +++ b/libs/halley/backbone/node.js @@ -0,0 +1,19 @@ +'use strict'; + +var Promise = require('bluebird'); +Promise.config({ + cancellation: true +}); + +require('../lib/util/externals').use({ + Events: require('backbone').Events, + extend: require('underscore').extend +}); + +var Transport = require('../lib/transport/transport'); + +/* Register the transports. Order is important */ +Transport.register('websocket' , require('../lib/transport/node/node-websocket')); +Transport.register('long-polling', require('../lib/transport/node/node-http')); + +module.exports = require('../lib/main'); diff --git a/libs/halley/backbone/package.json b/libs/halley/backbone/package.json new file mode 100644 index 0000000..daee103 --- /dev/null +++ b/libs/halley/backbone/package.json @@ -0,0 +1,6 @@ +{ + "name": "halley-backbone", + "version": "1.0.0", + "main": "./node.js", + "browser": "./browser.js" +} diff --git a/libs/halley/browser-standalone.js b/libs/halley/browser-standalone.js new file mode 100644 index 0000000..fc1eadf --- /dev/null +++ b/libs/halley/browser-standalone.js @@ -0,0 +1,19 @@ +'use strict'; + +var Promise = require('bluebird'); +Promise.config({ + cancellation: true +}); + +require('./lib/util/externals').use({ + Events: require('backbone-events-standalone'), + extend: require('lodash/object/extend') +}); + +var Transport = require('./lib/transport/transport'); + +/* Register the transports. Order is important */ +Transport.register('websocket' , require('./lib/transport/browser/browser-websocket')); +Transport.register('long-polling' , require('./lib/transport/browser/xhr')); + +module.exports = require('./lib/main'); diff --git a/libs/halley/gulpfile.js b/libs/halley/gulpfile.js new file mode 100644 index 0000000..209ac2c --- /dev/null +++ b/libs/halley/gulpfile.js @@ -0,0 +1,183 @@ +/* jshint node:true, unused:true */ +'use strict'; + +var gulp = require('gulp'); +var webpack = require('gulp-webpack'); +var gzip = require('gulp-gzip'); +var sourcemaps = require('gulp-sourcemaps'); +var gutil = require('gulp-util'); +var webpack = require('webpack'); +var uglify = require('gulp-uglify'); +var mocha = require('gulp-spawn-mocha'); +var KarmaServer = require('karma').Server; + +gulp.task("webpack-standalone", function(callback) { + // run webpack + webpack({ + entry: "./browser-standalone.js", + output: { + path: "dist/", + filename: "halley.js", + libraryTarget: "umd", + library: "Halley" + }, + stats: true, + failOnError: true, + node: { + console: false, + global: true, + process: false, + Buffer: false, + __filename: false, + __dirname: false, + setImmediate: false + }, + }, function(err, stats) { + if(err) throw new gutil.PluginError("webpack", err); + gutil.log("[webpack]", stats.toString({ + // output options + })); + callback(); + }); +}); + +gulp.task("webpack-backbone", function(callback) { + // run webpack + webpack({ + entry: "./backbone.js", + output: { + path: "dist/", + filename: "halley-backbone.js", + libraryTarget: "umd", + library: "Halley" + }, + externals: { + "backbone": "Backbone", + "underscore": "_" + }, + stats: true, + failOnError: true, + node: { + console: false, + global: true, + process: false, + Buffer: false, + __filename: false, + __dirname: false, + setImmediate: false + }, + }, function(err, stats) { + if(err) throw new gutil.PluginError("webpack", err); + gutil.log("[webpack]", stats.toString({ + // output options + })); + callback(); + }); +}); +gulp.task('webpack', ['webpack-backbone', 'webpack-standalone']); + +gulp.task('uglify', ['webpack'], function() { + return gulp.src('dist/*.js', { base: 'dist/' }) + .pipe(sourcemaps.init({ loadMaps: true })) + .pipe(uglify({ + + })) + .pipe(sourcemaps.write(".")) + .pipe(gulp.dest('dist/min')); +}); + +gulp.task('gzip', ['uglify'], function () { + return gulp.src(['dist/min/**'], { stat: true }) + .pipe(gzip({ append: true, gzipOptions: { level: 9 } })) + .pipe(gulp.dest('dist/min')); +}); + +gulp.task("webpack-test-suite-browser", function(callback) { + // run webpack + webpack({ + entry: "./test/integration/public/test-suite-browser.js", + output: { + path: "dist/", + filename: "test-suite-browser.js", + }, + stats: true, + resolve: { + alias: { + sinon: 'sinon-browser-only' + } + }, + module: { + noParse: [ + /sinon-browser-only/ + ] + }, + devtool: "#eval", + failOnError: true, + node: { + console: false, + global: true, + process: true, + Buffer: false, + __filename: false, + __dirname: false, + setImmediate: false + }, + }, function(err, stats) { + if(err) throw new gutil.PluginError("webpack", err); + gutil.log("[webpack]", stats.toString({ + // output options + })); + callback(); + }); +}); + +gulp.task('test-coverage', function() { + return gulp.src(['test/test-suite-node.js'], { read: false }) + .pipe(mocha({ + exposeGc: true, + istanbul: { + dir: 'dist/coverage' + } + })); +}); + +gulp.task('test', function() { + return gulp.src(['test/test-suite-node.js'], { read: false }) + .pipe(mocha({ + exposeGc: true + })); +}); + +gulp.task('karma', function (done) { + var fork = require('child_process').fork; + + var child = fork('./test/helpers/server'); + + process.on('exit', function() { + child.kill(); + }); + + setTimeout(function() { + function karmaComplete(err) { + if (err) { + console.log('ERROR IS') + console.log(err.stack); + } + child.kill(); + done(err); + } + + var karma = new KarmaServer({ + configFile: __dirname + '/karma.conf.js', + browsers: ['Firefox', 'Chrome', 'Safari'], + singleRun: true, + concurrency: 1 + }, karmaComplete); + + karma.start(); + + }, 1000); + +}); + +gulp.task('default', ['webpack', 'uglify', 'gzip', 'test-coverage']); diff --git a/libs/halley/index.js b/libs/halley/index.js new file mode 100644 index 0000000..4caf251 --- /dev/null +++ b/libs/halley/index.js @@ -0,0 +1,18 @@ +'use strict'; + +var Promise = require('bluebird'); +Promise.config({ + cancellation: true +}); + +require('./lib/util/externals').use({ + Events: require('backbone-events-standalone'), + extend: require('lodash/object/extend') +}); + +var Transport = require('./lib/transport/transport'); + +Transport.register('websocket' , require('./lib/transport/node/node-websocket')); +Transport.register('long-polling', require('./lib/transport/node/node-http')); + +module.exports = require('./lib/main'); diff --git a/libs/halley/karma.conf.js b/libs/halley/karma.conf.js new file mode 100644 index 0000000..2e01791 --- /dev/null +++ b/libs/halley/karma.conf.js @@ -0,0 +1,134 @@ +var webpack = require('webpack'); +var internalIp = require('internal-ip'); + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + + // list of files / patterns to load in the browser + files: [ + 'test/test-suite-browser.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/test-suite-browser.js': ['webpack'] + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + browserStack: { + username: process.env.BROWSERSTACK_USERNAME, + accessKey: process.env.BROWSERSTACK_KEY + }, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome', 'Firefox', 'Safari'], +/* + customLaunchers: { + bs_firefox_mac: { + base: 'BrowserStack', + browser: 'firefox', + browser_version: '21.0', + os: 'OS X', + os_version: 'Mountain Lion' + }, + bs_iphone5: { + base: 'BrowserStack', + device: 'iPhone 5', + os: 'ios', + os_version: '6.0' + } + }, + + browsers: ['bs_firefox_mac'], +*/ + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + webpack: { + resolve: { + alias: { + sinon: 'sinon-browser-only' + } + }, + module: { + noParse: [ + /sinon-browser-only/ + ] + }, + devtool: 'inline-source-map', + node: { + console: false, + global: true, + process: true, + Buffer: false, + __filename: false, + __dirname: false, + setImmediate: false + }, + plugins: [ + new webpack.DefinePlugin({ + HALLEY_TEST_SERVER: JSON.stringify('http://' + internalIp.v4() + ':8000') + }) + ] + }, + + webpackMiddleware: { + + }, + + plugins: [ + require("karma-mocha"), + require("karma-chrome-launcher"), + require("karma-firefox-launcher"), + require("karma-safari-launcher"), + require("karma-browserstack-launcher"), + require("karma-webpack") + ], + + captureTimeout: 60000, + // to avoid DISCONNECTED messages + browserDisconnectTimeout : 10000, // default 2000 + browserDisconnectTolerance : 1, // default 0 + browserNoActivityTimeout : 60000, //default 10000 + }); +}; diff --git a/libs/halley/lib/main.js b/libs/halley/lib/main.js new file mode 100644 index 0000000..9241e05 --- /dev/null +++ b/libs/halley/lib/main.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = { + Client: require('./protocol/client'), + VERSION: '2.0.0', + BAYEUX_VERSION: '1.0', + Promise: require('bluebird') +}; diff --git a/libs/halley/lib/mixins/statemachine-mixin.js b/libs/halley/lib/mixins/statemachine-mixin.js new file mode 100644 index 0000000..8e4d1c9 --- /dev/null +++ b/libs/halley/lib/mixins/statemachine-mixin.js @@ -0,0 +1,191 @@ +'use strict'; + +var Promise = require('bluebird'); +var Sequencer = require('../util/promise-util').Sequencer; +var cancelBarrier = require('../util/promise-util').cancelBarrier; +var debug = require('debug')('halley:fsm'); + +var StateMachineMixin = { + initStateMachine: function(config) { + this._config = config; + this._state = config.initial; + this._sequencer = new Sequencer(); + this._pendingTransitions = {}; + this._stateReset = false; + }, + + getState: function() { + return this._state; + }, + + stateIs: function() { + // 99% case optimisation + if (arguments.length === 1) { + return this._state === arguments[0]; + } + + for(var i = 0; i < arguments.length; i++) { + if(this._state === arguments[i]) return true; + } + + return false; + }, + + transitionState: function(transition, options) { + // No new states can be queued during a reset + if (this._stateReset) return Promise.reject(this._stateReset); + + var pending = this._pendingTransitions; + + if (options && options.dedup) { + // The caller can specify that it there is already + // a pending transition of the given type + // wait on that, rather than queueing another. + var pendingTransition = this._findPending(transition); + + if (pendingTransition) { + debug('transition state: %s (dedup)', transition); + return pendingTransition; + } + } + + var next = this._sequencer.chain(function() { + return this._dequeueTransition(transition, options); + }.bind(this)); + + if (!pending[transition]) { + // If this is the first transition of it's type + // save it for deduplication + pending[transition] = next; + } + + return next.finally(function() { + if (pending[transition] === next) { + delete pending[transition]; + } + }); + }, + + resetTransition: Promise.method(function(transition, reason, options) { + if (this._stateReset) return; // TODO: consider + + if (options && options.dedup) { + var pending = this._findPending(transition); + if (pending) { + debug('transition state: %s (dedup)', transition); + return pending; + } + } + + this._stateReset = reason; + + return this._sequencer.clear(reason) + .bind(this) + .then(function() { + this._stateReset = null; + return this.transitionState(transition, options); + }); + }), + + _findPending: function(transition) { + var pending = this._pendingTransitions; + + // The caller can specify that it there is already + // a pending transition of the given type + // wait on that, rather than queueing another. + return pending[transition]; + }, + + _dequeueTransition: Promise.method(function(transition, options) { + var optional = options && options.optional; + + debug('%s: Performing transition: %s', this._config.name, transition); + var newState = this._findTransition(transition); + if (!newState) { + if(!optional) { + throw new Error('Unable to perform transition ' + transition + ' from state ' + this._state); + } + + return null; + } + + if (newState === this._state) return null; + + debug('%s: leave: %s', this._config.name, this._state); + this._triggerStateLeave(this._state, newState); + + var oldState = this._state; + this._state = newState; + + debug('%s enter:%s', this._config.name, this._state); + var promise = this._triggerStateEnter(this._state, oldState) + .bind(this) + .catch(this._transitionError) + .then(function(nextTransition) { + if (nextTransition) { + if (this._stateReset) { + // No automatic transition on state reset + throw this._stateReset; + } + + return this._dequeueTransition(nextTransition); + } + + return null; + }) + + + // State transitions can't be cancelled + return cancelBarrier(promise); + }), + + /* Find the next state, given the current state and a transition */ + _findTransition: function(transition) { + var currentState = this._state; + var transitions = this._config.transitions; + var newState = transitions[currentState] && transitions[currentState][transition]; + if (newState) return newState; + + var globalTransitions = this._config.globalTransitions; + return globalTransitions && globalTransitions[transition]; + }, + + _triggerStateLeave: function(currentState, nextState) { + var handler = this['_onLeave' + currentState]; + if (handler) { + return handler.call(this, nextState); + } + }, + + _triggerStateEnter: Promise.method(function(newState, oldState) { + if (this.onStateChange) { + this.onStateChange(newState, oldState); + } + + var handler = this['_onEnter' + newState]; + if (handler) { + return handler.call(this, oldState) || null; + } + + return null; + }), + + _transitionError: function(err) { + // No automatic transitions on stateReset + if (this._stateReset) throw err; + + var state = this._state; + debug('Error while entering state %s: %s', state, err); + + // Check if the state has a error transition + var errorTransitionState = this._findTransition('error'); + if (errorTransitionState) { + return this._dequeueTransition('error'); + } + + /* No error handler, just throw */ + throw err; + } +}; + +module.exports = StateMachineMixin; diff --git a/libs/halley/lib/protocol/advice.js b/libs/halley/lib/protocol/advice.js new file mode 100644 index 0000000..7e93566 --- /dev/null +++ b/libs/halley/lib/protocol/advice.js @@ -0,0 +1,192 @@ +'use strict'; + +var Events = require('../util/externals').Events; +var extend = require('../util/externals').extend; +var debug = require('debug')('halley:advice'); + +var DEFAULT_MAX_NETWORK_DELAY = 30000; +var DEFAULT_ESTABLISH_TIMEOUT = 30000; +var DEFAULT_INTERVAL = 0; +var DEFAULT_TIMEOUT = 30000; +var DEFAULT_DISCONNECT_TIMEOUT = 10000; +var DEFAULT_RETRY_INTERVAL = 1000; +var DEFAULT_CONNECT_TIMEOUT_PADDING = 500; + +var ADVICE_HANDSHAKE = 'handshake'; +var ADVICE_RETRY = 'retry'; +var ADVICE_NONE = 'none'; + +/** + * After three successive failures, make sure we're not trying to quickly + */ +var HANDSHAKE_FAILURE_THRESHOLD = 3; +var HANDSHAKE_FAILURE_MIN_INTERVAL = 1000; + +var MAX_PING_INTERVAL = 50000; +var MIN_PING_INTERVAL = 1000; + +var MIN_ESTABLISH_TIMEOUT = 500; +var MAX_ESTABLISH_TIMEOUT = 60000; + +function Advice(options) { + /* Server controlled values */ + + var apply = function (key, defaultValue) { + if (options[key] !== undefined) { + this[key] = options[key]; + } else { + this[key] = defaultValue; + } + }.bind(this); + + /** + * An integer representing the minimum period of time, in milliseconds, for a + * client to delay subsequent requests to the /meta/connect channel. + * A negative period indicates that the message should not be retried. + * A client MUST implement interval support, but a client MAY exceed the + * interval provided by the server. A client SHOULD implement a backoff + * strategy to increase the interval if requests to the server fail without + * new advice being received from the server. + */ + apply('interval', DEFAULT_INTERVAL); + + /** + * An integer representing the period of time, in milliseconds, for the + * server to delay responses to the /meta/connect channel. + * This value is merely informative for clients. Bayeux servers SHOULD honor + * timeout advices sent by clients. + */ + apply('timeout', DEFAULT_TIMEOUT); + + /* Client values */ + + /** + * The amount of time to wait between successive retries on a + * single message send + */ + apply('retry', DEFAULT_RETRY_INTERVAL); + + + /** + * The maximum number of milliseconds to wait before considering a + * request to the Bayeux server failed. + */ + apply('maxNetworkDelay', DEFAULT_MAX_NETWORK_DELAY); + + /** + * The maximum number of milliseconds to wait for an HTTP connection to + * be opened, and return headers (but not body). In the case of a + * websocket, it's the amount of time to wait for an upgrade + */ + apply('establishTimeout', DEFAULT_ESTABLISH_TIMEOUT); + + /** + * Maximum time to wait on disconnect + */ + apply('disconnectTimeout', DEFAULT_DISCONNECT_TIMEOUT); + + /** + * Additional time to allocate to a connect message + * to avoid it timing out due to network latency. + */ + apply('connectTimeoutPadding', DEFAULT_CONNECT_TIMEOUT_PADDING); + + + + this._handshakes = 0; +} + +Advice.prototype = { + update: function(newAdvice) { + var advice = this; + + var adviceUpdated = false; + ['timeout', 'interval'].forEach(function(key) { + if (newAdvice[key] && newAdvice[key] !== advice[key]) { + adviceUpdated = true; + advice[key] = newAdvice[key]; + } + }); + + if (adviceUpdated) { + debug('Advice updated to interval=%s, timeout=%s using %j', this.interval, this.timeout, newAdvice); + } + + switch(newAdvice.reconnect) { + case ADVICE_HANDSHAKE: + this.trigger('advice:handshake'); + break; + + case ADVICE_NONE: + this.trigger('advice:none'); + break; + } + + }, + + handshakeFailed: function() { + this._handshakes++; + }, + + handshakeSuccess: function() { + this._handshakes = 0; + }, + + getMaxNetworkDelay: function() { + return Math.min(this.timeout, this.maxNetworkDelay); + }, + + getHandshakeInterval: function() { + var interval = this.interval; + + if (this._handshakes > HANDSHAKE_FAILURE_THRESHOLD && interval < HANDSHAKE_FAILURE_MIN_INTERVAL) { + return HANDSHAKE_FAILURE_MIN_INTERVAL; + } + + return interval; + }, + + getDisconnectTimeout: function() { + return Math.min(this.timeout, this.disconnectTimeout); + }, + + getPingInterval: function() { + // If the interval exceeds a minute theres a good chance an ELB or + // intermediate proxy will shut the connection down, so we set + // the interval to 50 seconds max + var pingInterval = this.timeout / 2; + return withinRange(pingInterval, MIN_PING_INTERVAL, MAX_PING_INTERVAL); + }, + + // This represents the amount of time allocated for a + // ``/meta/connect` message request-response cycle + // taking into account network latency + getConnectResponseTimeout: function() { + var padding = Math.min(this.connectTimeoutPadding, this.timeout / 4); + return this.timeout + padding; + }, + + getEstablishTimeout: function() { + // Upper-bound is either a minute, or three-quarters of the timeout value, + // whichever is lower + return withinRange(this.establishTimeout, + MIN_ESTABLISH_TIMEOUT, + Math.min(MAX_ESTABLISH_TIMEOUT, this.timeout * 0.75)); + } + +}; + +extend(Advice.prototype, Events); + + +/** + * Ensure `value` is within [min, max] + */ +function withinRange(value, min, max) { + if (isNaN(value)) return min; + if (value < min) return min; + if (value > max) return max; + return value; +} + +module.exports = Advice; diff --git a/libs/halley/lib/protocol/channel-set.js b/libs/halley/lib/protocol/channel-set.js new file mode 100644 index 0000000..0663749 --- /dev/null +++ b/libs/halley/lib/protocol/channel-set.js @@ -0,0 +1,109 @@ +'use strict'; + +var Channel = require('./channel'); +var Promise = require('bluebird'); +var Synchronized = require('../util/promise-util').Synchronized; +var debug = require('debug')('halley:channel-set'); + +function ChannelSet(onSubscribe, onUnsubscribe) { + this._onSubscribe = onSubscribe; + this._onUnsubscribe = onUnsubscribe; + this._channels = {}; + this._syncronized = new Synchronized(); +} + +ChannelSet.prototype = { + get: function(name) { + return this._channels[name]; + }, + + getKeys: function() { + return Object.keys(this._channels); + }, + + /** + * Returns a promise of a subscription + */ + subscribe: function(channelName, subscription) { + // All subscribes and unsubscribes are synchonized by + // the channel name to prevent inconsistent state + return this._sync(channelName, function() { + return this._syncSubscribe(channelName, subscription); + }); + }, + + unsubscribe: function(channelName, subscription) { + // All subscribes and unsubscribes are synchonized by + // the channel name to prevent inconsistent state + return this._sync(channelName, function() { + return this._syncUnsubscribe(channelName, subscription); + }); + }, + + reset: function() { + this._channels = {}; + this._syncronized = new Synchronized(); + }, + + _sync: function(channelName, fn) { + return this._syncronized.sync(channelName, fn.bind(this)); + }, + + _syncSubscribe: Promise.method(function(name, subscription) { + debug('subscribe: channel=%s', name); + + var existingChannel = this._channels[name]; + + // If the client is resubscribing to an existing channel + // there is no need to re-issue to message to the server + if (existingChannel) { + debug('subscribe: existing: channel=%s', name); + + existingChannel.add(subscription); + return subscription; + } + + return this._onSubscribe(name) + .bind(this) + .then(function(response) { + debug('subscribe: success: channel=%s', name); + + var channel = this._channels[name] = new Channel(name); + channel.add(subscription); + + subscription._subscribeSuccess(response); + + return subscription; + }); + }), + + _syncUnsubscribe: Promise.method(function(name, subscription) { + debug('unsubscribe: channel=%s', name); + var channel = this._channels[name]; + if (!channel) return; + + channel.remove(subscription); + + // Do not perform the `unsubscribe` if the channel is still being used + // by other subscriptions + if (!channel.isUnused()) return; + + delete this._channels[name]; + + return this._onUnsubscribe(name); + }), + + distributeMessage: function(message) { + var channels = Channel.expand(message.channel); + + for (var i = 0, n = channels.length; i < n; i++) { + var channel = this._channels[channels[i]]; + if (channel) { + channel.receive(message.data); + } + } + } + +}; + +module.exports = ChannelSet; diff --git a/libs/halley/lib/protocol/channel.js b/libs/halley/lib/protocol/channel.js new file mode 100644 index 0000000..c4babd6 --- /dev/null +++ b/libs/halley/lib/protocol/channel.js @@ -0,0 +1,90 @@ +'use strict'; + +var extend = require('../util/externals').extend; + +var GRAMMAR_CHANNEL_NAME = /^\/[a-zA-Z0-9\-\_\!\~\(\)\$\@]+(\/[a-zA-Z0-9\-\_\!\~\(\)\$\@]+)*$/; +var GRAMMAR_CHANNEL_PATTERN = /^(\/[a-zA-Z0-9\-\_\!\~\(\)\$\@]+)*\/\*{1,2}$/; + +function Channel(name) { + this.id = this.name = name; + this._subscriptions = []; +} + +Channel.prototype = { + add: function(subscription) { + this._subscriptions.push(subscription); + }, + + remove: function(subscription) { + this._subscriptions = this._subscriptions.filter(function(s) { + return s !== subscription; + }); + }, + + receive: function(message) { + var subscriptions = this._subscriptions; + + for (var i = 0; i < subscriptions.length; i++) { + subscriptions[i]._receive(message); + } + }, + + subscribeSuccess: function(response) { + var subscriptions = this._subscriptions; + + for (var i = 0; i < subscriptions.length; i++) { + subscriptions[i]._subscribeSuccess(response); + } + }, + + isUnused: function() { + return !this._subscriptions.length; + } +}; + +/* Statics */ +extend(Channel, { + HANDSHAKE: '/meta/handshake', + CONNECT: '/meta/connect', + SUBSCRIBE: '/meta/subscribe', + UNSUBSCRIBE: '/meta/unsubscribe', + DISCONNECT: '/meta/disconnect', + + META: 'meta', + SERVICE: 'service', + + isValid: function(name) { + return GRAMMAR_CHANNEL_NAME.test(name) || + GRAMMAR_CHANNEL_PATTERN.test(name); + }, + + parse: function(name) { + if (!this.isValid(name)) return null; + return name.split('/').slice(1); + }, + + unparse: function(segments) { + return '/' + segments.join('/'); + }, + + expand: function(name) { + var segments = this.parse(name), + channels = ['/**', name]; + + var copy = segments.slice(); + copy[copy.length - 1] = '*'; + channels.push(this.unparse(copy)); + + for (var i = 1, n = segments.length; i < n; i++) { + copy = segments.slice(0, i); + copy.push('**'); + channels.push(this.unparse(copy)); + } + + return channels; + } + + +}); + +module.exports = Channel; diff --git a/libs/halley/lib/protocol/client.js b/libs/halley/lib/protocol/client.js new file mode 100644 index 0000000..dcf3825 --- /dev/null +++ b/libs/halley/lib/protocol/client.js @@ -0,0 +1,525 @@ +'use strict'; + +var Extensions = require('./extensions'); +var BayeuxError = require('../util/errors').BayeuxError; +var TransportError = require('../util/errors').TransportError; +var Channel = require('./channel'); +var ChannelSet = require('./channel-set'); +var Dispatcher = require('./dispatcher'); +var Promise = require('bluebird'); +var debug = require('debug')('halley:client'); +var StateMachineMixin = require('../mixins/statemachine-mixin'); +var extend = require('../util/externals').extend; +var Events = require('../util/externals').Events; +var globalEvents = require('../util/global-events'); +var Advice = require('./advice'); +var Subscription = require('./subscription'); +var SubscribeThenable = require('./subscribe-thenable'); + + +var MANDATORY_CONNECTION_TYPES = ['long-polling']; +var DEFAULT_ENDPOINT = '/bayeux'; + +/** + * TODO: make the states/transitions look more like the official client states + * http://docs.cometd.org/reference/bayeux_operation.html#d0e9971 + */ +var FSM = { + name: 'client', + initial: 'UNCONNECTED', + globalTransitions: { + disable: 'DISABLED', + disconnect: 'UNCONNECTED' + }, + transitions: { + // The client is not yet connected + UNCONNECTED: { + connect: 'HANDSHAKING', + reset: 'HANDSHAKING' + }, + // The client is undergoing the handshake process + HANDSHAKING: { + handshakeSuccess: 'CONNECTED', + rehandshake: 'HANDSHAKE_WAIT', // TODO:remove + error: 'HANDSHAKE_WAIT' + }, + // Handshake failed, try again after interval + HANDSHAKE_WAIT: { + timeout: 'HANDSHAKING' + }, + // The client is connected + CONNECTED: { + disconnect: 'DISCONNECTING', + rehandshake: 'HANDSHAKE_WAIT', + reset: 'RESETTING' + }, + // The client is undergoing reset + // RESETTING is handled by the same handler as disconnect, so must + // support the same transitions (with different states) + RESETTING: { + disconnectSuccess: 'HANDSHAKING', + reset: 'HANDSHAKING', + connect: 'HANDSHAKING', + error: 'HANDSHAKING' + }, + // The client is disconnecting + DISCONNECTING: { + disconnectSuccess: 'UNCONNECTED', + reset: 'HANDSHAKING', + connect: 'HANDSHAKING', + error: 'UNCONNECTED' + }, + // The client has been disabled an will not reconnect + // after being sent { advice: none } + DISABLED: { + + } + } +}; + +function validateBayeuxResponse(response) { + if (!response) { + throw new TransportError('No response received'); + } + + if (!response.successful) { + throw new BayeuxError(response.error); + } + + return response; +} + +/** + * The Halley Client + */ +function Client(endpoint, options) { + debug('New client created for %s', endpoint); + if (!options) options = {}; + + var advice = this._advice = new Advice(options); + + debug('Initial advice: %j', this._advice); + + this._extensions = new Extensions(); + this._endpoint = endpoint || DEFAULT_ENDPOINT; + this._channels = new ChannelSet(this._onSubscribe.bind(this), this._onUnsubscribe.bind(this)); + this._dispatcher = options.dispatcher || new Dispatcher(this._endpoint, advice, options); + this._initialConnectionTypes = options.connectionTypes || MANDATORY_CONNECTION_TYPES; + this._messageId = 0; + this._connected = false; + + /** + * How many times have we failed handshaking + */ + this.initStateMachine(FSM); + + this.listenTo(this._dispatcher, 'message', this._receiveMessage); + + this.listenTo(advice, 'advice:handshake', function() { + return this.transitionState('rehandshake', { optional: true, dedup: true }); + }); + + this.listenTo(advice, 'advice:none', function() { + return this.resetTransition('disable', new Error('Client disabled')); + }); + + this.listenTo(this._dispatcher, 'transport:up transport:down', function() { + this._updateConnectionState(); + }); + +} + +Client.prototype = { + addExtension: function(extension) { + this._extensions.add(extension); + }, + + removeExtension: function(extension) { + this._extensions.remove(extension); + }, + + handshake: function() { + debug('handshake'); + return this.transitionState('connect', { optional: true }); + }, + + /** + * Wait for the client to connect + */ + connect: Promise.method(function() { + if (this.stateIs('DISABLED')) throw new Error('Client disabled'); + if (this.stateIs('CONNECTED')) return; + + return this.transitionState('connect', { optional: true }); + }), + + disconnect: Promise.method(function() { + if (this.stateIs('DISABLED')) return; + if (this.stateIs('UNCONNECTED')) return; + + return this.resetTransition('disconnect', new Error('Client disconnected'), { dedup: true }); + }), + + /** + * Returns a thenable of a subscription + */ + subscribe: function(channelName, onMessage, context, onSubscribe) { + var subscription = new Subscription(this._channels, channelName, onMessage, context, onSubscribe); + var subscribePromise = this._channels.subscribe(channelName, subscription); + return new SubscribeThenable(subscribePromise); + }, + + /** + * Publish a message + * @return {Promise} A promise of the response + */ + publish: function(channel, data, options) { + debug('publish: channel=%s, data=%j', channel, data); + + return this.connect() + .bind(this) + .then(function() { + return this._sendMessage({ + channel: channel, + data: data + }, options); + }) + .then(validateBayeuxResponse); + }, + + /** + * Resets the client and resubscribes to existing channels + * This can be used when the client is in an inconsistent state + */ + reset: function() { + debug('reset'); + return this.transitionState('reset', { optional: true }); + }, + + /** + * Returns the clientId or null + */ + getClientId: function() { + return this._dispatcher.clientId; + }, + + listChannels: function() { + return this._channels.getKeys(); + }, + + _onSubscribe: function(channel) { + return this.connect() + .bind(this) + .then(function() { + return this._sendMessage({ + channel: Channel.SUBSCRIBE, + subscription: channel + }); + }) + .then(validateBayeuxResponse); + }, + + _onUnsubscribe: function(channel) { + return this.connect() + .bind(this) + .then(function() { + return this._sendMessage({ + channel: Channel.UNSUBSCRIBE, + subscription: channel + }); + }) + .then(validateBayeuxResponse); + }, + + _updateConnectionState: function() { + // The client is connected when the state + // of the client is CONNECTED and the + // transport is up + var isConnected = this.stateIs('CONNECTED') && this._dispatcher.isTransportUp(); + if (this._connected === isConnected) return; + this._connected = isConnected; + + debug('Connection state changed to %s', isConnected ? 'up' : 'down'); + + this.trigger(isConnected ? 'connection:up' : 'connection:down'); + }, + + onStateChange: function() { + this._updateConnectionState(); + }, + + /** + * The client must issue a handshake with the server + */ + _onEnterHANDSHAKING: function() { + this._dispatcher.clientId = null; + + return this._dispatcher.selectTransport(this._initialConnectionTypes) + .bind(this) + .then(function() { + return this._sendMessage({ + channel: Channel.HANDSHAKE + }, { + attempts: 1 // Note: only try once + }); + }) + .then(function(response) { + validateBayeuxResponse(response); + + this._dispatcher.clientId = response.clientId; + var supportedConnectionTypes = this._supportedTypes = response.supportedConnectionTypes; + + debug('Handshake successful: %s', this._dispatcher.clientId); + + return this._dispatcher.selectTransport(supportedConnectionTypes, true); + }) + .return('handshakeSuccess'); + + }, + + /** + * Handshake has failed. Waits `interval` ms then + * attempts another handshake + */ + _onEnterHANDSHAKE_WAIT: function() { + this._advice.handshakeFailed(); + + var delay = this._advice.getHandshakeInterval(); + + debug('Waiting %sms before rehandshaking', delay); + return Promise.delay(delay) + .return('timeout'); + }, + + /** + * The client has connected. It needs to send out regular connect + * messages. + */ + _onEnterCONNECTED: function() { + this.trigger('connected'); + + // Fire a disconnect when the user navigates away + this.listenTo(globalEvents, 'beforeunload', this.disconnect); + + /* Handshake success, reset count */ + this._advice.handshakeSuccess(); + + this._sendConnect(); + + this._resubscribeAll() // Not chained + .catch(function(err) { + debug('resubscribe all failed on connect: %s', err); + }) + .done(); + + return Promise.resolve(); + }, + + /** + * Stop sending connect messages + */ + _onLeaveCONNECTED: function() { + if (this._connect) { + debug('Cancelling pending connect request'); + this._connect.cancel(); + this._connect = null; + } + + // Fire a disconnect when the user navigates away + this.stopListening(globalEvents); + }, + + /** + * The client will attempt a disconnect and will + * transition back to the HANDSHAKING state + */ + _onEnterRESETTING: function() { + debug('Resetting %s', this._dispatcher.clientId); + return this._onEnterDISCONNECTING(); + }, + + /** + * The client is disconnecting, or resetting. + */ + _onEnterDISCONNECTING: function() { + debug('Disconnecting %s', this._dispatcher.clientId); + + return this._sendMessage({ + channel: Channel.DISCONNECT + }, { + attempts: 1, + timeout: this._advice.getDisconnectTimeout() + }) + .bind(this) + .then(validateBayeuxResponse) + .return('disconnectSuccess') + .finally(function() { + this._dispatcher.close(); + + this.trigger('disconnect'); + }); + }, + + /** + * The client is no longer connected. + */ + _onEnterUNCONNECTED: function() { + this._dispatcher.clientId = null; + this._dispatcher.close(); + debug('Clearing channel listeners for %s', this._dispatcher.clientId); + this._channels.reset(); + }, + + /** + * The server has told the client to go away and + * don't come back + */ + _onEnterDISABLED: function() { + this._onEnterUNCONNECTED(); + this.trigger('disabled'); + }, + + /** + * Use to resubscribe all previously subscribed channels + * after re-handshaking + */ + _resubscribeAll: Promise.method(function() { + var channels = this._channels; + var channelNames = channels.getKeys(); + + return Promise.map(channelNames, function(channelName) { + debug('Client attempting to resubscribe to %s', channelName); + var channel = channels.get(channelName); + + return this._sendMessage({ + channel: Channel.SUBSCRIBE, + subscription: channelName + }) + .then(validateBayeuxResponse) + .tap(function(response) { + channel.subscribeSuccess(response); + }); + + }.bind(this)); + + }), + + /** + * Send a request message to the server, to which a reply should + * be received. + * + * @return Promise of response + */ + _sendMessage: function(message, options) { + message.id = this._generateMessageId(); + + return this._extensions.pipe('outgoing', message) + .bind(this) + .then(function(message) { + if (!message) return; + + return this._dispatcher.sendMessage(message, options) + .bind(this) + .then(function(response) { + return this._extensions.pipe('incoming', response); + }); + }); + + }, + + /** + * Event handler for when a message has been received through a channel + * as opposed to as the result of a request. + */ + _receiveMessage: function(message) { + this._extensions.pipe('incoming', message) + .bind(this) + .then(function(message) { + if (!message) return; + + if (!message || !message.channel || message.data === undefined) return; + this._channels.distributeMessage(message); + return null; + }) + .done(); + }, + + /** + * Generate a unique messageid + */ + _generateMessageId: function() { + this._messageId += 1; + if (this._messageId >= Math.pow(2, 32)) this._messageId = 0; + return this._messageId.toString(36); + }, + + /** + * Periodically fire a connect message with `interval` ms between sends + * Ensures that multiple connect messages are not fired simultaneously. + * + * From the docs: + * The client MUST maintain only a single outstanding connect message. + * If the server does not have a current outstanding connect and a connect + * is not received within a configured timeout, then the server + * SHOULD act as if a disconnect message has been received. + */ + _sendConnect: function() { + if (this._connect) { + debug('Cancelling pending connect request'); + this._connect.cancel(); + this._connect = null; + } + + this._connect = this._sendMessage({ + channel: Channel.CONNECT + }, { + timeout: this._advice.getConnectResponseTimeout() + }) + .bind(this) + .then(validateBayeuxResponse) + .catch(function(err) { + debug('Connect failed: %s', err); + }) + .finally(function() { + this._connect = null; + + // If we're no longer connected so don't re-issue the + // connect again + if (!this.stateIs('CONNECTED')) { + return null; + } + + var interval = this._advice.interval; + + debug('Will connect after interval: %sms', interval); + this._connect = Promise.delay(interval) + .bind(this) + .then(function() { + this._connect = null; + + // No longer connected after the interval, don't re-issue + if (!this.stateIs('CONNECTED')) { + return; + } + + /* Do not chain this */ + this._sendConnect(); + + // Return an empty promise to stop + // bluebird from raising warnings + return Promise.resolve(); + }); + + // Return an empty promise to stop + // bluebird from raising warnings + return Promise.resolve(); + }); + } + +}; + +/* Mixins */ +extend(Client.prototype, Events); +extend(Client.prototype, StateMachineMixin); + +module.exports = Client; diff --git a/libs/halley/lib/protocol/dispatcher.js b/libs/halley/lib/protocol/dispatcher.js new file mode 100644 index 0000000..b581082 --- /dev/null +++ b/libs/halley/lib/protocol/dispatcher.js @@ -0,0 +1,298 @@ +'use strict'; + +var Scheduler = require('./scheduler'); +var Transport = require('../transport/transport'); +var Channel = require('./channel'); +var TransportPool = require('../transport/pool'); +var uri = require('../util/uri'); +var extend = require('../util/externals').extend; +var Events = require('../util/externals').Events; +var debug = require('debug')('halley:dispatcher'); +var Promise = require('bluebird'); +var Envelope = require('./envelope'); +var TransportError = require('../util/errors').TransportError; +var danglingFinally = require('../util/promise-util').danglingFinally; + +var HANDSHAKE = 'handshake'; +var BAYEUX_VERSION = '1.0'; + +var STATE_UP = 1; +var STATE_DOWN = 2; + +/** + * The dispatcher sits between the client and the transport. + * + * It's responsible for tracking sending messages to the transport, + * tracking in-flight messages + */ +function Dispatcher(endpoint, advice, options) { + this._advice = advice; + this._envelopes = {}; + this._scheduler = options.scheduler || Scheduler; + this._state = 0; + this._pool = new TransportPool(this, uri.parse(endpoint), advice, options.disabled, Transport.getRegisteredTransports()); + +} + +Dispatcher.prototype = { + + destroy: function() { + debug('destroy'); + + this.close(); + }, + + close: function() { + debug('_close'); + + this._cancelPending(); + + debug('Dispatcher close requested'); + this._pool.close(); + }, + + _cancelPending: function() { + var envelopes = this._envelopes; + this._envelopes = {}; + var envelopeKeys = Object.keys(envelopes); + + debug('_cancelPending %s envelopes', envelopeKeys.length); + envelopeKeys.forEach(function(id) { + var envelope = envelopes[id]; + envelope.reject(new Error('Dispatcher closed')); + }, this); + }, + + getConnectionTypes: function() { + return Transport.getConnectionTypes(); + }, + + selectTransport: function(allowedTransportTypes, cleanup) { + return this._pool.setAllowed(allowedTransportTypes, cleanup); + }, + + /** + * Returns a promise of the response + */ + sendMessage: function(message, options) { + var id = message.id; + var envelopes = this._envelopes; + var advice = this._advice; + + var envelope = envelopes[id] = new Envelope(message); + + var timeout; + if (options && options.timeout) { + timeout = options.timeout; + } else { + timeout = advice.timeout; + } + + var scheduler = new this._scheduler(message, { + timeout: timeout, + interval: advice.retry, + attempts: options && options.attempts + }); + + var promise = this._attemptSend(envelope, message, scheduler); + if (options && options.deadline) { + promise = promise.timeout(options && options.deadline, 'Timeout on deadline'); + } + + return danglingFinally(promise, function() { + debug('sendMessage finally: message=%j', message); + + if (promise.isFulfilled()) { + scheduler.succeed(); + } else { + scheduler.abort(); + } + + delete envelopes[id]; + }); + }, + + _attemptSend: function(envelope, message, scheduler) { + if (!scheduler.isDeliverable()) { + return Promise.reject(new Error('No longer deliverable')); + } + + scheduler.send(); + + var timeout = scheduler.getTimeout(); + + // 1. Obtain transport + return this._pool.get() + .bind(this) + .then(function(transport) { + debug('attemptSend: %j', message); + envelope.startSend(transport); + + // 2. Send the message using the given transport + var enrichedMessage = this._enrich(message, transport); + + return transport.sendMessage(enrichedMessage); + }) + .then(function() { + this._triggerUp(); + + // 3. Wait for the response from the transport + return envelope.awaitResponse(); + }) + .timeout(timeout, 'Timeout on message send') + .finally(function() { + envelope.stopSend(); + }) + .then(function(response) { + // 4. Parse the response + + if (response.successful === false && response.advice && response.advice.reconnect === HANDSHAKE) { + // This is not standard, and may need a bit of reconsideration + // but if the client sends a message to the server and the server responds with + // an error and tells the client it needs to rehandshake, + // reschedule the send after the send after the handshake has occurred. + throw new TransportError('Message send failed with advice reconnect:handshake, will reschedule send'); + } + + return response; + }) + .catch(Promise.TimeoutError, TransportError, function(e) { + debug('Error while attempting to send message: %j: %s', message, e); + + this._triggerDown(); + scheduler.fail(); + + if (!scheduler.isDeliverable()) { + throw e; + } + + // Either the send timed out or no transport was + // available. Either way, wait for the interval and try again + return this._awaitRetry(envelope, message, scheduler); + }); + + }, + + /** + * Adds required fields into the message + */ + _enrich: function(message, transport) { + if (message.channel === Channel.CONNECT) { + message.connectionType = transport.connectionType; + } + + if (message.channel === Channel.HANDSHAKE) { + message.version = BAYEUX_VERSION; + message.supportedConnectionTypes = this.getConnectionTypes(); + } else { + if (!this.clientId) { + // Theres probably a nicer way of doing this. If the connection + // is in the process of being re-established, throw an error + // for non-handshake messages which will cause them to be rescheduled + // in future, hopefully once the client is CONNECTED again + throw new Error('client is not yet established'); + } + message.clientId = this.clientId; + } + + return message; + }, + + /** + * Send has failed. Retry after interval + */ + _awaitRetry: function(envelope, message, scheduler) { + // Either no transport is available or a timeout occurred waiting for + // the transport. Wait a bit, the try again + return Promise.delay(scheduler.getInterval()) + .bind(this) + .then(function() { + return this._attemptSend(envelope, message, scheduler); + }); + + }, + + handleResponse: function(reply) { + if (reply.advice) this._advice.update(reply.advice); + var id = reply.id; + var envelope = id && this._envelopes[id]; + + if (reply.successful !== undefined && envelope) { + // This is a response to a message we fired. + envelope.resolve(reply); + } else { + // Distribe this message through channels + // Don't trigger a message if this is a reply + // to a request, otherwise it'll pass + // through the extensions twice + this.trigger('message', reply); + } + }, + + _triggerDown: function() { + if (this._state === STATE_DOWN) return; + debug('Dispatcher is DOWN'); + + this._state = STATE_DOWN; + this.trigger('transport:down'); + }, + + _triggerUp: function() { + if (this._state === STATE_UP) return; + debug('Dispatcher is UP'); + + this._state = STATE_UP; + this.trigger('transport:up'); + + // If we've disable websockets due to a network + // outage, try re-enable them now + this._pool.reevaluate(); + }, + + /** + * Called by transports on connection error + */ + handleError: function(transport) { + // This method may be called from outside the eventloop + // so queue the method to ensure any finally methods + // on pending promises are called prior to executing + Promise.resolve() + .bind(this) + .then(function() { + var envelopes = this._envelopes; + + // If the transport goes down, reject any outstanding + // connect messages. We don't reject non-connect messages + // as we assume that they've been sent to the server + // already, and we don't need to resend them. If they had failed + // to send, the send would have been rejected already. + // As a failback, the message timeout will eventually + // result in a rejection anyway. + Object.keys(envelopes).forEach(function(id) { + var envelope = envelopes[id]; + + var message = envelope.message; + if (envelope.transport === transport && message && message.channel === Channel.CONNECT) { + envelope.reject(new Error('Transport failed')); + } + }); + + // If this transport is the current, + // report the connection as down + if (transport === this._pool.current()) { + this._triggerDown(); + } + + this._pool.down(transport); + }); + }, + + isTransportUp: function() { + return this._state === STATE_UP; + } +}; + +/* Mixins */ +extend(Dispatcher.prototype, Events); + +module.exports = Dispatcher; diff --git a/libs/halley/lib/protocol/envelope.js b/libs/halley/lib/protocol/envelope.js new file mode 100644 index 0000000..cf609fa --- /dev/null +++ b/libs/halley/lib/protocol/envelope.js @@ -0,0 +1,56 @@ +'use strict'; + +var Promise = require('bluebird'); + +/** + * An envelope represents a single request and response + * with one or more send requests. + * + * - Sends cannot overlap. + * - When a send is rejected, the dispatcher will queue up + * another send. However the response will only be + * accepted or rejected once + */ +function Envelope() { + this.transport = null; + this._sendPromise = null; + this._sendResolve = null; + this._sendReject = null; +} + +Envelope.prototype = { + + startSend: function(transport) { + this.transport = transport; + this._sendPromise = new Promise(function(resolve, reject) { + this._sendResolve = resolve; + this._sendReject = reject; + }.bind(this)); + + }, + + stopSend: function() { + this.transport = null; + this._sendPromise = null; + this._sendResolve = null; + this._sendReject = null; + }, + + resolve: function(value) { + if (this._sendResolve) { + this._sendResolve(value); + } + }, + + reject: function(reason) { + if (this._sendReject) { + this._sendReject(reason); + } + }, + + awaitResponse: function() { + return this._sendPromise; + } +}; + +module.exports = Envelope; diff --git a/libs/halley/lib/protocol/extensions.js b/libs/halley/lib/protocol/extensions.js new file mode 100644 index 0000000..0da67c2 --- /dev/null +++ b/libs/halley/lib/protocol/extensions.js @@ -0,0 +1,51 @@ +'use strict'; + +var Promise = require('bluebird'); + +function Extensions() { + this._extensions = []; +} + +Extensions.prototype = { + add: function(extension) { + this._extensions.push(extension); + }, + + remove: function(extension) { + this._extensions = this._extensions.filter(function(e) { + return e !== extension; + }); + }, + + pipe: function(stage, message) { + var extensions = this._extensions; + + if (!extensions || extensions.length === 0) return Promise.resolve(message); + + extensions = extensions.filter(function(extension) { + return extension && extension[stage]; + }); + + if (!extensions.length) return Promise.resolve(message); + + // Since most of the extensions are synchronous, using + // a callback style iterator tends to be an order + // of magnitude faster than a Promise.reduce style + // function here + return new Promise(function(resolve) { + var index = 0; + (function next(message) { + var current = index++; + if (current >= extensions.length) { + return resolve(message); + } + + var extension = extensions[current]; + var fn = extension[stage]; + fn.call(extension, message, next); + })(message); + }); + } +}; + +module.exports = Extensions; diff --git a/libs/halley/lib/protocol/scheduler.js b/libs/halley/lib/protocol/scheduler.js new file mode 100644 index 0000000..a35032b --- /dev/null +++ b/libs/halley/lib/protocol/scheduler.js @@ -0,0 +1,70 @@ +'use strict'; + +var REPEATED_ATTEMPT_FAILURE_THRESHOLD = 3; +var MIN_INTERVAL_ON_REPEAT_FAILURE = 5000; + +/** + * Handles the scheduling of a single message + */ +function Scheduler(message, options) { + this.message = message; + this.options = options; + this.attempts = 0; + this.failures = 0; + this.finished = false; +} + +Scheduler.prototype = { + getTimeout: function() { + return this.options.timeout; + }, + + getInterval: function() { + if (this.attempts >= REPEATED_ATTEMPT_FAILURE_THRESHOLD) { + return Math.max(this.options.interval, MIN_INTERVAL_ON_REPEAT_FAILURE); + } + return this.options.interval; + }, + + isDeliverable: function() { + if (this.finished) return false; + var allowedAttempts = this.options.attempts; + + if (!allowedAttempts) return true; + + // Say we have 3 attempts... + // On the 3rd failure, it's not deliverable + // On the 3rd attempts, it's deliverable + return this.attempts <= allowedAttempts && this.failures < allowedAttempts; + }, + + /** + * Called immediately prior to resending + */ + send: function() { + this.attempts++; + }, + + /** + * Called when an attempt to send has failed + */ + fail: function() { + this.failures++; + }, + + /** + * Called when the message has been sent successfully + */ + succeed: function() { + this.finished = true; + }, + + /** + * Called when the message is aborted + */ + abort: function() { + this.finished = true; + } +}; + +module.exports = Scheduler; diff --git a/libs/halley/lib/protocol/subscribe-thenable.js b/libs/halley/lib/protocol/subscribe-thenable.js new file mode 100644 index 0000000..08c6629 --- /dev/null +++ b/libs/halley/lib/protocol/subscribe-thenable.js @@ -0,0 +1,44 @@ +'use strict'; + +var Promise = require('bluebird'); + +function SubscribeThenable(subscribePromise) { + this._promise = subscribePromise; +} + +SubscribeThenable.prototype = { + then: function(resolve, reject) { + return this._promise.then(resolve, reject); + }, + + catch: function(reject) { + return this._promise.catch(reject); + }, + + bind: function(context) { + return this._promise.bind(context); + }, + + unsubscribe: Promise.method(function() { + if (this.isRejected()) return; + + return this._promise.then(function(subscription) { + return subscription.unsubscribe(); + }); + }), + + // Useful for testing + isPending: function() { + return this._promise.isPending(); + }, + + isFulfilled: function() { + return this._promise.isFulfilled(); + }, + + isRejected: function() { + return this._promise.isRejected(); + } +}; + +module.exports = SubscribeThenable; diff --git a/libs/halley/lib/protocol/subscription.js b/libs/halley/lib/protocol/subscription.js new file mode 100644 index 0000000..44944dd --- /dev/null +++ b/libs/halley/lib/protocol/subscription.js @@ -0,0 +1,33 @@ +'use strict'; + +var Promise = require('bluebird'); + +function Subscription(channels, channel, onMessage, context, onSubscribe) { + this._channels = channels; + this._channel = channel; + this._onMessage = onMessage; + this._context = context; + this._onSubscribe = onSubscribe; +} + +Subscription.prototype = { + + _receive: function(message) { + if (this._onMessage) { + this._onMessage.call(this._context, message); + } + }, + + _subscribeSuccess: function(response) { + if (this._onSubscribe) { + this._onSubscribe.call(this._context, response); + } + }, + + unsubscribe: Promise.method(function() { + return this._channels.unsubscribe(this._channel, this); + }) + +}; + +module.exports = Subscription; diff --git a/libs/halley/lib/transport/base-long-polling-transport.js b/libs/halley/lib/transport/base-long-polling-transport.js new file mode 100644 index 0000000..713f4b8 --- /dev/null +++ b/libs/halley/lib/transport/base-long-polling-transport.js @@ -0,0 +1,59 @@ +'use strict'; + +var Batcher = require('../util/promise-util').Batcher; +var Channel = require('../protocol/channel'); +var debug = require('debug')('halley:batching-transport'); +var inherits = require('inherits'); +var Transport = require('./transport'); +var extend = require('../util/externals').extend; +var TransportError = require('../util/errors').TransportError; + +var MAX_DELAY = 8; + +function findConnectMessage(messages) { + for (var i = 0; i < messages.length; i++) { + var message = messages[i]; + if (message.channel === Channel.CONNECT) return message; + } + + return null; +} + +function BaseLongPollingTransport(dispatcher, endpoint, advice) { + BaseLongPollingTransport.super_.call(this, dispatcher, endpoint, advice); + this._batcher = new Batcher(this._dequeue.bind(this), MAX_DELAY); +} +inherits(BaseLongPollingTransport, Transport); + +extend(BaseLongPollingTransport.prototype, { + close: function() { + this._batcher.destroy(new TransportError('Transport closed')); + }, + + /* Returns a promise of a request */ + sendMessage: function(message) { + var sendImmediate = message.channel === Channel.HANDSHAKE; + + return this._batcher.add(message, sendImmediate); + }, + + _dequeue: function(outbox) { + debug('Flushing batch of %s messages', outbox.length); + + if (outbox.length > 1) { + var connectMessage = findConnectMessage(outbox); + + // If we have sent out a request. don't + // long poll on the response. Instead request + // an immediate response from the server + if (connectMessage) { + connectMessage.advice = { timeout: 0 }; + } + } + + return this.request(outbox); + } + +}); + +module.exports = BaseLongPollingTransport; diff --git a/libs/halley/lib/transport/base-websocket.js b/libs/halley/lib/transport/base-websocket.js new file mode 100644 index 0000000..b5e56aa --- /dev/null +++ b/libs/halley/lib/transport/base-websocket.js @@ -0,0 +1,252 @@ +'use strict'; + +var Transport = require('./transport'); +var uri = require('../util/uri'); +var Promise = require('bluebird'); +var debug = require('debug')('halley:websocket'); +var inherits = require('inherits'); +var extend = require('../util/externals').extend; +var globalEvents = require('../util/global-events'); +var TransportError = require('../util/errors').TransportError; + +var WS_CONNECTING = 0; +var WS_OPEN = 1; +var WS_CLOSING = 2; +var WS_CLOSED = 3; + +var PROTOCOLS = { + 'http:': 'ws:', + 'https:': 'wss:' +}; + +var openSocketsCount = 0; + +function getSocketUrl(endpoint) { + endpoint = extend({ }, endpoint); + endpoint.protocol = PROTOCOLS[endpoint.protocol]; + return uri.stringify(endpoint); +} + +function WebSocketTransport(dispatcher, endpoint, advice) { + WebSocketTransport.super_.call(this, dispatcher, endpoint, advice); + + this._pingTimer = null; + this._pingResolves = null; + this._connectPromise = this._createConnectPromise(); +} +inherits(WebSocketTransport, Transport); + +extend(WebSocketTransport.prototype, { + /* Abstract _createWebsocket: function(url) { } */ + + /** + * Connects and returns a promise that resolves when the connection is + * established + */ + connect: function() { + return this._connectPromise || Promise.reject(new TransportError('Socket disconnected')); + }, + + close: function(error) { + /* Only perform close once */ + if (!this._connectPromise) return; + this._connectPromise = null; + openSocketsCount--; + + this._error(error || new TransportError('Websocket transport closed')); + + clearTimeout(this._pingTimer); + + globalEvents.off('network', this._pingNow, this); + globalEvents.off('sleep', this._pingNow, this); + + var socket = this._socket; + if (socket) { + debug('Closing websocket'); + + this._socket = null; + + var state = socket.readyState; + socket.onerror = socket.onclose = socket.onmessage = null; + + if(state === WS_OPEN || state === WS_CONNECTING) { + socket.close(); + } + } + }, + + /* Returns a request */ + request: function(messages) { + return this.connect() + .bind(this) + .then(function() { + var socket = this._socket; + if (!socket || socket.readyState !== WS_OPEN) { + throw new TransportError('Websocket unavailable'); + } + + socket.send(JSON.stringify(messages)); + }) + .catch(function(e) { + this.close(e); + throw e; + }); + }, + + /** + * Returns a promise of a connected socket. + */ + _createConnectPromise: function() { + debug('Entered connecting state, creating new WebSocket connection'); + + var url = getSocketUrl(this.endpoint); + var socket = this._socket = this._createWebsocket(url); + + return new Promise(function(resolve, reject, onCancel) { + if (!socket) { + return reject(new TransportError('Sockets not supported')); + } + + openSocketsCount++; + switch (socket.readyState) { + case WS_OPEN: + resolve(socket); + break; + + case WS_CONNECTING: + break; + + case WS_CLOSING: + case WS_CLOSED: + reject(new TransportError('Socket connection failed')); + return; + } + + socket.onopen = function() { + resolve(socket); + }; + + var self = this; + socket.onmessage = function(e) { + debug('Received message: %s', e.data); + self._onmessage(e); + }; + + socket.onerror = function() { + debug('WebSocket error'); + var err = new TransportError("Websocket error"); + self.close(err); + reject(err); + }; + + socket.onclose = function(e) { + debug('Websocket closed. code=%s reason=%s', e.code, e.reason); + var err = new TransportError("Websocket connection failed: code=" + e.code + ": " + e.reason); + self.close(err); + reject(err); + }; + + onCancel(function() { + debug('Closing websocket connection on cancelled'); + self.close(); + }); + + }.bind(this)) + .bind(this) + .timeout(this._advice.getEstablishTimeout(), 'Websocket connect timeout') + .then(function(socket) { + // Connect success, setup listeners + this._pingTimer = setTimeout(this._pingInterval.bind(this), this._advice.getPingInterval()); + + globalEvents.on('network', this._pingNow, this); + globalEvents.on('sleep', this._pingNow, this); + return socket; + }) + .catch(function(e) { + this.close(e); + throw e; + }); + }, + + _onmessage: function(e) { + var replies = JSON.parse(e.data); + if (!replies) return; + + /* Resolve any outstanding pings */ + if (this._pingResolves) { + this._pingResolves.forEach(function(resolve) { + resolve(); + }); + + this._pingResolves = null; + } + + replies = [].concat(replies); + + this._receive(replies); + }, + + _ping: function() { + debug('ping'); + + return this.connect() + .bind(this) + .then(function(socket) { + // Todo: deal with a timeout situation... + if(socket.readyState !== WS_OPEN) { + throw new TransportError('Socket not open'); + } + + var resolve; + var promise = new Promise(function(res) { + resolve = res; + }); + + var resolvesQueue = this._pingResolves; + if (resolvesQueue) { + resolvesQueue.push(resolve); + } else { + this._pingResolves = [resolve]; + } + + socket.send("[]"); + + return promise; + }) + .timeout(this._advice.getMaxNetworkDelay(), 'Ping timeout') + .catch(function(err) { + this.close(err); + throw err; + }); + }, + + /** + * If we have reason to believe that the connection may be flaky, for + * example, the computer has been asleep for a while, we send a ping + * immediately (don't batch with other ping replies) + */ + _pingNow: function() { + debug('Ping invoked on event'); + this._ping() + .catch(function(err) { + debug('Ping failure: closing socket: %s', err); + }); + }, + + _pingInterval: function() { + this._ping() + .bind(this) + .then(function() { + this._pingTimer = setTimeout(this._pingInterval.bind(this), this._advice.getPingInterval()); + }) + .catch(function(err) { + debug('Interval ping failure: closing socket: %s', err); + }); + } +}); + +WebSocketTransport._countSockets = function() { + return openSocketsCount; +}; + +module.exports = WebSocketTransport; diff --git a/libs/halley/lib/transport/browser/.jshintrc b/libs/halley/lib/transport/browser/.jshintrc new file mode 100644 index 0000000..f5dff8c --- /dev/null +++ b/libs/halley/lib/transport/browser/.jshintrc @@ -0,0 +1,5 @@ +{ + "node": true, + "browser": true, + "unused": true +} diff --git a/libs/halley/lib/transport/browser/browser-websocket.js b/libs/halley/lib/transport/browser/browser-websocket.js new file mode 100644 index 0000000..fafeb68 --- /dev/null +++ b/libs/halley/lib/transport/browser/browser-websocket.js @@ -0,0 +1,31 @@ +'use strict'; + +var inherits = require('inherits'); +var extend = require('../../util/externals').extend; +var BaseWebSocket = require('../base-websocket'); + +function BrowserWebSocket(dispatcher, endpoint, advice) { + BrowserWebSocket.super_.call(this, dispatcher, endpoint, advice); +} +inherits(BrowserWebSocket, BaseWebSocket); + +extend(BrowserWebSocket.prototype, { + _createWebsocket: function(url) { + if (window.MozWebSocket) { + return new window.MozWebSocket(url); + } + + if (window.WebSocket) { + return new window.WebSocket(url); + } + }, +}); + + +/* Statics */ +BrowserWebSocket.create = BaseWebSocket.create; +BrowserWebSocket.isUsable = function(/*endpoint*/) { + return window.MozWebSocket || window.WebSocket; +}; + +module.exports = BrowserWebSocket; diff --git a/libs/halley/lib/transport/browser/xhr.js b/libs/halley/lib/transport/browser/xhr.js new file mode 100644 index 0000000..73e1368 --- /dev/null +++ b/libs/halley/lib/transport/browser/xhr.js @@ -0,0 +1,134 @@ +'use strict'; + +var BaseLongPollingTransport = require('../base-long-polling-transport'); +var debug = require('debug')('halley:xhr'); +var inherits = require('inherits'); +var extend = require('../../util/externals').extend; +var Promise = require('bluebird'); +var TransportError = require('../../util/errors').TransportError; + +var XML_HTTP_HEADERS_RECEIVED = 2; +var XML_HTTP_LOADING = 3; +var XML_HTTP_DONE = 4; + +var WindowXMLHttpRequest = window.XMLHttpRequest; + +function XHRTransport(dispatcher, endpoint) { + XHRTransport.super_.call(this, dispatcher, endpoint); + this._sameOrigin = isSameOrigin(endpoint); +} +inherits(XHRTransport, BaseLongPollingTransport); + +extend(XHRTransport.prototype, { + encode: function(messages) { + var stringified = JSON.stringify(messages); + if (this._sameOrigin) { + // Same origin requests have proper content-type set, so they + // can use application/json + return stringified; + } else { + // CORS requests are posted as plain text + return 'message=' + encodeURIComponent(stringified); + } + }, + + request: function(messages) { + return new Promise(function(resolve, reject, onCancel) { + var href = this.endpoint.href; + var xhr = new WindowXMLHttpRequest(); + var self = this; + + xhr.open('POST', href, true); + + // Don't set headers for CORS requests + if (this._sameOrigin) { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Pragma', 'no-cache'); + } + + function cleanup() { + if (!xhr) return; + xhr.onreadystatechange = null; + xhr = null; + } + + xhr.onreadystatechange = function() { + if (!xhr) return; + + var readyState = xhr.readyState; + + if (readyState !== XML_HTTP_DONE && + readyState !== XML_HTTP_HEADERS_RECEIVED && + readyState !== XML_HTTP_LOADING) return; + + /** + * XMLHTTPRequest implementation in MSXML HTTP (at least in IE 8.0 on Windows XP SP3+) + * does not handle HTTP responses with status code 204 (No Content) properly; + * the `status' property has the value 1223. + */ + var status = xhr.status; + var successful = (status >= 200 && status < 300) || status === 304 || status === 1223; + if (successful) { + resolve(); + } else { + var err = new TransportError('HTTP Status ' + status); + reject(err); + self._error(err); + return; + } + + if (xhr.readyState != XML_HTTP_DONE) { + return; + } + + /* readyState is XML_HTTP_DONE */ + var text = xhr.responseText; + cleanup(); + + var replies; + try { + replies = JSON.parse(text); + } catch (e) { + debug('Unable to parse XHR response: %s', e); + self._error(new TransportError('Server response parse failure')); + return; + } + + if (replies && Array.isArray(replies)) { + self._receive(replies); + } else { + self._error(new TransportError('Invalid response from server')); + } + }; + + xhr.send(this.encode(messages)); + + /* Cancel the XHR request */ + onCancel(function() { + if (!xhr) return; + xhr.onreadystatechange = null; + if (xhr.readyState !== XML_HTTP_DONE) { + xhr.abort(); + } + cleanup(); + }); + + }.bind(this)); + } +}); + +/* Statics */ +XHRTransport.isUsable = function() { + return true; +}; + +function isSameOrigin(uri) { + var location = window.location; + if (!location) return false; + return uri.protocol === location.protocol && + uri.hostname === location.hostname && + uri.port === location.port; +} + + +module.exports = XHRTransport; diff --git a/libs/halley/lib/transport/node/.jshintrc b/libs/halley/lib/transport/node/.jshintrc new file mode 100644 index 0000000..07282f5 --- /dev/null +++ b/libs/halley/lib/transport/node/.jshintrc @@ -0,0 +1,5 @@ +{ + "node": true, + "browser": false, + "unused": true +} diff --git a/libs/halley/lib/transport/node/node-http.js b/libs/halley/lib/transport/node/node-http.js new file mode 100644 index 0000000..7c7c3c9 --- /dev/null +++ b/libs/halley/lib/transport/node/node-http.js @@ -0,0 +1,109 @@ +'use strict'; + +var BaseLongPollingTransport = require('../base-long-polling-transport'); +var http = require('http'); +var https = require('https'); +var inherits = require('inherits'); +var extend = require('../../util/externals').extend; +var Promise = require('bluebird'); +var TransportError = require('../../util/errors').TransportError; + +function NodeHttpTransport(dispatcher, endpoint, advice) { + NodeHttpTransport.super_.call(this, dispatcher, endpoint, advice); + + var endpointSecure = this.endpoint.protocol === 'https:'; + this._httpClient = endpointSecure ? https : http; +} +inherits(NodeHttpTransport, BaseLongPollingTransport); + +extend(NodeHttpTransport.prototype, { + encode: function(messages) { + return JSON.stringify(messages); + }, + + request: function(messages) { + + return new Promise(function(resolve, reject, onCancel) { + var self = this; + var content = new Buffer(this.encode(messages), 'utf8'); + var params = this._buildParams(content); + + var request = this._httpClient.request(params, function(response) { + var status = response.statusCode; + var successful = (status >= 200 && status < 300); + if (successful) { + resolve(); + } else { + var err = new TransportError('HTTP Status ' + status); + reject(err); + self._error(err); + return; + } + + var body = ''; + + response.setEncoding('utf8'); + response.on('data', function(chunk) { body += chunk; }); + + response.on('end', function() { + var replies = null; + + try { + replies = JSON.parse(body); + } catch (e) { + self._error(new TransportError('Server response parse failure')); + return; + } + + if (replies && Array.isArray(replies)) { + self._receive(replies); + } else { + self._error(new TransportError('Invalid response from server')); + } + }); + }); + + request.setSocketKeepAlive(true, this._advice.getPingInterval()); + request.once('error', function(error) { + var err = new TransportError(error.message); + reject(err); + self._error(new TransportError('Invalid response from server')); + }); + + request.end(content); + onCancel(function() { + request.abort(); + request.removeAllListeners(); + }); + + }.bind(this)); + }, + + _buildParams: function(content) { + var uri = this.endpoint; + + var params = { + method: 'POST', + host: uri.hostname, + path: uri.path, + headers: { + 'Content-Length': content.length, + 'Content-Type': 'application/json', + 'Host': uri.host + } + }; + + if (uri.port) { + params.port = uri.port; + } + + return params; + } +}); + +/* Statics */ +NodeHttpTransport.isUsable = function(endpoint) { + return (endpoint.protocol === 'http:' || endpoint.protocol === 'https:') && endpoint.host && endpoint.path; +}; + +module.exports = NodeHttpTransport; diff --git a/libs/halley/lib/transport/node/node-websocket.js b/libs/halley/lib/transport/node/node-websocket.js new file mode 100644 index 0000000..07584d1 --- /dev/null +++ b/libs/halley/lib/transport/node/node-websocket.js @@ -0,0 +1,24 @@ +'use strict'; + +var inherits = require('inherits'); +var extend = require('../../util/externals').extend; +var WebSocket = require('faye-websocket'); +var BaseWebSocket = require('../base-websocket'); + +function NodeWebSocket(dispatcher, endpoint, advice) { + NodeWebSocket.super_.call(this, dispatcher, endpoint, advice); +} +inherits(NodeWebSocket, BaseWebSocket); + +extend(NodeWebSocket.prototype, { + _createWebsocket: function(url) { + return new WebSocket.Client(url, [], { extensions: this._dispatcher.wsExtensions }); + }, +}); + + +NodeWebSocket.create = BaseWebSocket.create; +NodeWebSocket.isUsable = function(endpoint) { + return (endpoint.protocol === 'http:' || endpoint.protocol === 'https:') && endpoint.host && endpoint.path; +}; +module.exports = NodeWebSocket; diff --git a/libs/halley/lib/transport/pool.js b/libs/halley/lib/transport/pool.js new file mode 100644 index 0000000..d075e4a --- /dev/null +++ b/libs/halley/lib/transport/pool.js @@ -0,0 +1,185 @@ +'use strict'; + +var debug = require('debug')('halley:pool'); +var Promise = require('bluebird'); +var cancelBarrier = require('../util/promise-util').cancelBarrier; +var LazySingleton = require('../util/promise-util').LazySingleton; + +function TransportPool(dispatcher, endpoint, advice, disabled, registered) { + this._dispatcher = dispatcher; + this._endpoint = endpoint; + this._advice = advice; + this._transports = {}; + this._disabled = disabled; + this._registered = registered; + this._current = new LazySingleton(this._reselect.bind(this)); + + this._registeredHash = registered.reduce(function(memo, transport) { + var type = transport[0]; + var Klass = transport[1]; + memo[type] = Klass; + return memo; + }, {}); + + this._allowed = null; + + this.setAllowed(null) + .catch(function(err) { + debug('Unable to preconnect to any available transports: err=%s', err); + }) + .done(); +} + +TransportPool.prototype = { + /** Returns a promise to transport */ + get: function() { + var current = this._current.get(); + return cancelBarrier(current); + }, + + current: function() { + var c = this._current.peek(); + + if (c && c.isFulfilled()) { + return c.value(); + } + }, + + /** + * Set the allowed transport types that `.get` will return + */ + setAllowed: function(allowedTypes, cleanup) { + // Maintain the order from this._allowed + this._allowed = this._registered + .map(function(transport) { + return transport[0]; + }) + .filter(function(type) { + return !allowedTypes || allowedTypes.indexOf(type) >= 0; + }); + + if (cleanup) { + // Remove transports that we won't use + Object.keys(this._transports).forEach(function(type) { + if (this._allowed.indexOf(type) >= 0) return; + + var transport = this._transports[type]; + delete this._transports[type]; + + if (transport.isFulfilled()) { + transport.value().close(); + } else { + transport.cancel(); + } + + }, this); + } + + return this.reevaluate(); + }, + + reevaluate: function() { + this._current.clear(); + var current = this._current.get(); + return cancelBarrier(current); + }, + + _reselect: function() { + var allowed = this._allowed; + debug('_reselect: %j', allowed); + + // Load the transport + var connectionPromises = allowed + .filter(function(type) { + var Klass = this._registeredHash[type]; + + if (this._disabled && this._disabled.indexOf(type) >= 0) return false; + + return Klass.isUsable(this._endpoint); + }, this) + .map(function(type) { + var Klass = this._registeredHash[type]; + + var current = this._transports[type]; + if (current) { + if(!current.isRejected() && !current.isCancelled()) { + return current; + } + // Should we cancel the current? + } + + var instance = new Klass(this._dispatcher, this._endpoint, this._advice); + + var promise = instance.connect ? + instance.connect().return(instance) : + Promise.resolve(instance); + + this._transports[type] = promise; + return promise; + }, this); + + if (!connectionPromises.length) { + return Promise.reject(new Error('No suitable transports available')); + } + + // Return the first usable transport + return Promise.any(connectionPromises) + .then(function(transport) { + debug('Selected transport %s', transport.connectionType); + return transport; + }) + .catch(Promise.AggregateError, function(err) { + /* Fail with the first problem */ + throw err[0]; + }); + }, + + close: function() { + debug('_close'); + + var transports = this._transports; + this._transports = {}; + + var current = this._current.value; + if (current) { + + this._current.clear(); + if (current.isPending()) { + current.cancel(); + } + } + + Object.keys(transports).forEach(function(type) { + var transportPromise = transports[type]; + if (transportPromise.isFulfilled()) { + transportPromise.value().close(); + } else { + transportPromise.cancel(); + } + }); + }, + + /** + * Called on transport close + */ + down: function(transport) { + var connectionType = transport.connectionType; + var transportPromise = this._transports[connectionType]; + if (!transportPromise) return; + + if (transportPromise.isFulfilled()) { + var existingTransport = transportPromise.value(); + if (existingTransport !== transport) return; + + // Don't call transport.close as this + // will be called from the close + delete this._transports[connectionType]; + + // Next time someone does a `.get` we will attempt to reselect + this._current.clear(); + } + } + +}; + +module.exports = TransportPool; diff --git a/libs/halley/lib/transport/transport.js b/libs/halley/lib/transport/transport.js new file mode 100644 index 0000000..406a107 --- /dev/null +++ b/libs/halley/lib/transport/transport.js @@ -0,0 +1,56 @@ +'use strict'; + +var debug = require('debug')('halley:transport'); + +var registeredTransports = []; + +function Transport(dispatcher, endpoint, advice) { + this._dispatcher = dispatcher; + this._advice = advice; + this.endpoint = endpoint; +} + +Transport.prototype = { + close: function() { + }, + + /* Abstract encode: function(messages) { } */ + /* Abstract request: function(messages) { } */ + + /* Returns a promise of a request */ + sendMessage: function(message) { + return this.request([message]); + }, + + _receive: function(replies) { + if (!replies) return; + replies = [].concat(replies); + + debug('Received via %s: %j', this.connectionType, replies); + + for (var i = 0, n = replies.length; i < n; i++) { + this._dispatcher.handleResponse(replies[i]); + } + }, + + _error: function(error) { + this._dispatcher.handleError(this, error); + } + +}; + +/* Statics */ +Transport.getRegisteredTransports = function() { + return registeredTransports; +}; + +Transport.register = function(type, klass) { + registeredTransports.push([type, klass]); + klass.prototype.connectionType = type; +}; + +Transport.getConnectionTypes = function() { + return registeredTransports.map(function(t) { return t[0]; }); +}; + +module.exports = Transport; diff --git a/libs/halley/lib/util/errors.js b/libs/halley/lib/util/errors.js new file mode 100644 index 0000000..883250c --- /dev/null +++ b/libs/halley/lib/util/errors.js @@ -0,0 +1,58 @@ +'use strict'; + +function defineProperty(obj, name, value) { + Object.defineProperty(obj, name, { + value: value, + configurable: true, + enumerable: false, + writable: true + }); +} + +function BayeuxError(message) { + if (!(this instanceof BayeuxError)) return new BayeuxError(message); + + var code, params, m; + message = message || ''; + var match = /^([\d]+):([^:]*):(.*)$/.exec(message); + + if (match) { + code = parseInt(match[1], 10); + params = match[2].split(','); + m = match[3]; + } + + defineProperty(this, "message", m || message || "bayeuxError"); + defineProperty(this, "name", "BayeuxError"); + defineProperty(this, "code", code); + defineProperty(this, "params", params); + defineProperty(this, "bayeuxMessage", m); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } else { + Error.call(this); + } +} +BayeuxError.prototype = Object.create(Error.prototype); +BayeuxError.prototype.constructor = BayeuxError; + +function TransportError(message) { + if (!(this instanceof TransportError)) return new TransportError(message); + + defineProperty(this, "message", message); + defineProperty(this, "name", "TransportError"); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } else { + Error.call(this); + } +} +TransportError.prototype = Object.create(Error.prototype); +TransportError.prototype.constructor = TransportError; + +module.exports = { + BayeuxError: BayeuxError, + TransportError: TransportError +}; diff --git a/libs/halley/lib/util/externals.js b/libs/halley/lib/util/externals.js new file mode 100644 index 0000000..64870fe --- /dev/null +++ b/libs/halley/lib/util/externals.js @@ -0,0 +1,11 @@ +'use strict'; + +var dependencies = { + use: function(deps) { + Object.keys(deps).forEach(function(key) { + dependencies[key] = deps[key]; + }); + } +}; + +module.exports = dependencies; diff --git a/libs/halley/lib/util/global-events.js b/libs/halley/lib/util/global-events.js new file mode 100644 index 0000000..492d5a5 --- /dev/null +++ b/libs/halley/lib/util/global-events.js @@ -0,0 +1,61 @@ +/* jshint browser:true */ +'use strict'; + +var Events = require('../util/externals').Events; +var extend = require('../util/externals').extend; +var debug = require('debug')('halley:global-events'); +// var ReactNative = require('react-native') +// var NetInfo = ReactNative.NetInfo + +var SLEEP_TIMER_INTERVAL = 30000; + +var globalEvents = {}; +extend(globalEvents, Events); + +/* Only install the listener in a browser environment */ +if (typeof window !== 'undefined') { + install(); +} + +installSleepDetector(); + +module.exports = globalEvents; + +function fire(type) { + return function(e) { + debug(type); + globalEvents.trigger(type, e); + }; +} + +function install() { +// window.addEventListener('online', fire('network'), false); +// window.addEventListener('offline', fire('network'), false); + +// var navigatorConnection = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection); + +// if (navigatorConnection) { +// navigatorConnection.addEventListener('typechange', fire('network'), false); +// } +} + +function installSleepDetector() { + var sleepLast = Date.now(); + + var timer = setInterval(function() { + var now = Date.now(); + + if(sleepLast - now > SLEEP_TIMER_INTERVAL * 2) { + /* Sleep detected */ + debug('sleep'); + globalEvents.trigger('sleep'); + } + + sleepLast = now; + }, SLEEP_TIMER_INTERVAL); + + // Unreference the timer in nodejs + if (timer.unref) { + timer.unref(); + } +} diff --git a/libs/halley/lib/util/promise-util.js b/libs/halley/lib/util/promise-util.js new file mode 100644 index 0000000..bb6156f --- /dev/null +++ b/libs/halley/lib/util/promise-util.js @@ -0,0 +1,303 @@ + +'use strict'; + +var Promise = require('bluebird'); + +exports.danglingFinally = danglingFinally; +exports.Synchronized = Synchronized; +exports.LazySingleton = LazySingleton; +exports.cancelBarrier = cancelBarrier; +exports.after = after; +exports.Throttle = Throttle; +exports.Batcher = Batcher; +exports.Sequencer = Sequencer; + +/** + * Adds a finally clause not chained to the original + * promise, which allows the `fn` called to use + * reflection methods like `isFulfilled` + * + * Catches any errors to prevent bluebird warnings. + * The other fork of the promise should handle the + * real exception chain + */ +function danglingFinally(promise, fn, context) { + promise.catch(function() {}) + .finally(function() { + fn.call(context); + return null; + }); + + return promise; +} +/** + * Returns a promise which will always resolve after the provided + * promise is no longer pending. Will resolve even if the upstream + * promise is cancelled. + */ +function after(promise) { + if (!promise.isPending()) return Promise.resolve(); + + return new Promise(function(resolve) { + danglingFinally(promise, function() { + return resolve(); + }); + }); +} + +/* Prevent a cancel from propogating upstream */ +function cancelBarrier(promise) { + if (!promise.isPending()) return promise; + + return new Promise(function(resolve, reject) { + return promise.then(resolve, reject); + }); +} + +function LazySingleton(factory) { + this.value = null; + this._factory = factory; +} + +LazySingleton.prototype = { + get: function() { + var value = this.value; + if (value) { + return value; + } + + value = this.value = Promise.try(this._factory); + + return value + .bind(this) + .finally(function() { + if (value !== this.value) return; + + if (!value.isFulfilled()) { + this.value = null; + } + }); + }, + + peek: function() { + return this.value; + }, + + clear: function() { + this.value = null; + } +}; + +function Synchronized() { + this._keys = {}; +} + +Synchronized.prototype = { + sync: function(key, fn) { + var keys = this._keys; + var pending = keys[key]; + + if (pending) { + // Append to the end and wait + pending = keys[key] = after(pending) + .bind(this) + .then(function() { + if (pending === keys[key]) { + delete keys[key]; + } + + return fn(); + }); + } else { + // Execute immediately + pending = keys[key] = Promise.try(fn) + .finally(function() { + if (pending === keys[key]) { + delete keys[key]; + } + }); + } + + return pending; + } +}; + +function Throttle(fn, delay) { + this._fn = fn; + this._delay = delay; + this._next = null; + this._resolveNow = null; + this._rejectNow = null; + this._timer = null; +} + +Throttle.prototype = { + fire: function(forceImmediate) { + if (this._next) { + if (forceImmediate) { + this._resolveNow(); + } + + // Return a fork of the promise + return this._next.tap(function() { }); + } + + var promise = this._next = new Promise(function(resolve, reject) { + this._resolveNow = resolve; + this._rejectNow = reject; + + if (forceImmediate) { + resolve(); + } else { + this._timer = setTimeout(function() { + this._timer = null; + resolve(); + }.bind(this), this._delay); + } + }.bind(this)) + .bind(this) + .finally(this._cleanup) + .then(function() { + return this._fn(); + }); + + // Return a fork of the promise + return promise.tap(function() {}); + }, + + _cleanup: function() { + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + } + + this._next = null; + this._fireNow = null; + this._rejectNow = null; + }, + + destroy: function(e) { + if (this._rejectNow) { + this._rejectNow(e); + } + this._cleanup(); + } +}; + +function Batcher(fn, delay) { + this._throttle = new Throttle(this._dequeue.bind(this), delay); + this._fn = Promise.method(fn); + this._pending = []; +} + +Batcher.prototype = { + add: function(value, forceImmediate) { + var defer = { value: undefined, promise: undefined }; + + var resolve, reject; + var promise = new Promise(function(res, rej) { + resolve = res; + reject = rej; + }); + + defer.value = value; + defer.promise = promise; + + this._pending.push(defer); + + this._throttle.fire(forceImmediate) + .then(resolve, reject); + + return promise; + }, + + next: function(forceImmediate) { + return this._throttle.fire(forceImmediate); + }, + + _dequeue: function() { + var pending = this._pending; + this._pending = []; + + var values = pending.filter(function(defer) { + return !defer.promise.isCancelled(); + }).map(function(defer) { + return defer.value; + }); + + if (!values.length) return; + + return this._fn(values); + }, + + destroy: function(e) { + this._throttle.destroy(e); + this._pending = []; + } +}; + +/** + * The sequencer will chain a series of promises together + * one after the other. + * + * It will also handle rejections and cancelations + */ +function Sequencer() { + this._queue = []; + this._executing = false; +} + +Sequencer.prototype = { + chain: function(fn) { + var queue = this._queue; + var resolve, reject; + + var promise = new Promise(function(res, rej) { + resolve = res; + reject = rej; + }).then(fn); + + queue.push({ resolve: resolve, reject: reject, promise: promise }); + this._dequeue(); + + return promise; + }, + + _dequeue: function() { + if (this._executing) return; + var queue = this._queue; + + var next = queue.pop(); + if (!next) return; + + next.resolve(); + + this._executing = next.promise; + danglingFinally(next.promise, function() { + this._executing = null; + this._dequeue(); + }, this); + }, + + /** + * Removes all items from the queue and rejects them with + * the supplied error. + * + * Returns a promise which will always resolve once any outstanding + * promises are finalised + */ + clear: function(err) { + var queue = this._queue; + this._queue = []; + + queue.forEach(function(item) { + item.reject(err); + }); + + if (this._executing) { + return after(this._executing); + } else { + return Promise.resolve(); + } + } +}; diff --git a/libs/halley/lib/util/uri.js b/libs/halley/lib/util/uri.js new file mode 100644 index 0000000..52cbb0b --- /dev/null +++ b/libs/halley/lib/util/uri.js @@ -0,0 +1,77 @@ +'use strict'; + +/* node-safe reference to the window */ +var win = typeof window === 'object' && window; // jshint ignore:line + +module.exports = { + + parse: function(url) { + if (typeof url !== 'string') return url; + var uri = {}, parts, query, pairs, i, n, data; + + var consume = function(name, pattern) { + url = url.replace(pattern, function(match) { + uri[name] = match; + return ''; + }); + uri[name] = uri[name] || ''; + }; + + consume('protocol', /^[a-z]+\:/i); + consume('host', /^\/\/[^\/\?#]+/); + + if (!/^\//.test(url) && !uri.host) + url = win.location.pathname.replace(/[^\/]*$/, '') + url; + + consume('pathname', /^[^\?#]*/); + consume('search', /^\?[^#]*/); + consume('hash', /^#.*/); + + uri.protocol = uri.protocol || win.location.protocol; + + if (uri.host) { + uri.host = uri.host.substr(2); + parts = uri.host.split(':'); + uri.hostname = parts[0]; + uri.port = parts[1] || ''; + } else { + uri.host = win.location.host; + uri.hostname = win.location.hostname; + uri.port = win.location.port; + } + + uri.pathname = uri.pathname || '/'; + uri.path = uri.pathname + uri.search; + + query = uri.search.replace(/^\?/, ''); + pairs = query ? query.split('&') : []; + data = {}; + + for (i = 0, n = pairs.length; i < n; i++) { + parts = pairs[i].split('='); + data[decodeURIComponent(parts[0] || '')] = decodeURIComponent(parts[1] || ''); + } + + uri.query = data; + + uri.href = this.stringify(uri); + return uri; + }, + + stringify: function(uri) { + var string = uri.protocol + '//' + uri.hostname; + if (uri.port) string += ':' + uri.port; + string += uri.pathname + this.queryString(uri.query) + (uri.hash || ''); + return string; + }, + + queryString: function(query) { + var pairs = []; + for (var key in query) { + if (!query.hasOwnProperty(key)) continue; + pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(query[key])); + } + if (pairs.length === 0) return ''; + return '?' + pairs.join('&'); + } +}; diff --git a/libs/halley/package.json b/libs/halley/package.json new file mode 100644 index 0000000..f71bef6 --- /dev/null +++ b/libs/halley/package.json @@ -0,0 +1,140 @@ +{ + "_args": [ + [ + { + "raw": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", + "scope": null, + "escapedName": null, + "name": null, + "rawSpec": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", + "spec": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", + "type": "hosted", + "hosted": { + "type": "github", + "ssh": "git@github.com:terrysahaidak/react-native-halley.git#master", + "sshUrl": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", + "httpsUrl": "git+https://github.com/terrysahaidak/react-native-halley.git#master", + "gitUrl": "git://github.com/terrysahaidak/react-native-halley.git#master", + "shortcut": "github:terrysahaidak/react-native-halley#master", + "directUrl": "https://raw.githubusercontent.com/terrysahaidak/react-native-halley/master/package.json" + } + }, + "/Users/terry/Projects/Gitter/GitterMobile" + ] + ], + "_from": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", + "_id": "halley@0.5.2", + "_inCache": true, + "_installable": true, + "_location": "/halley", + "_phantomChildren": {}, + "_requested": { + "raw": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", + "scope": null, + "escapedName": null, + "name": null, + "rawSpec": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", + "spec": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", + "type": "hosted", + "hosted": { + "type": "github", + "ssh": "git@github.com:terrysahaidak/react-native-halley.git#master", + "sshUrl": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", + "httpsUrl": "git+https://github.com/terrysahaidak/react-native-halley.git#master", + "gitUrl": "git://github.com/terrysahaidak/react-native-halley.git#master", + "shortcut": "github:terrysahaidak/react-native-halley#master", + "directUrl": "https://raw.githubusercontent.com/terrysahaidak/react-native-halley/master/package.json" + } + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#279aa5f20df2d08d27a60e3386b80af8ab9a5fd3", + "_shasum": "1fafc6f92484ed8dd8beff4d0dd0280a0dbf6018", + "_shrinkwrap": null, + "_spec": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", + "_where": "/Users/terry/Projects/Gitter/GitterMobile", + "author": { + "name": "Andrew Newdigate", + "email": "andrew@gitter.im" + }, + "browser": "./browser-standalone", + "bugs": { + "url": "https://github.com/gitterHQ/halley/issues" + }, + "dependencies": { + "backbone": "1.2.3", + "backbone-events-standalone": "git://github.com/suprememoocow/backbone-events-standalone.git#e3cf6aaf0742d655687296753836339dcf0ff483", + "bluebird": "^3.3.1", + "debug": "^2.0.0", + "faye-websocket": "~0.10.0", + "inherits": "^2.0.0", + "lodash": "^3.0.0" + }, + "description": "A bayeux client for modern browsers and node. Forked from Faye", + "devDependencies": { + "coveralls": "^2.11.4", + "express": "^4.13.3", + "gitter-faye": "^1.1.0-h", + "gulp": "^3.9.0", + "gulp-gzip": "^1.2.0", + "gulp-sourcemaps": "^1.6.0", + "gulp-spawn-mocha": "^2.2.1", + "gulp-uglify": "^1.5.1", + "gulp-util": "^3.0.7", + "gulp-webpack": "^1.5.0", + "imports-loader": "^0.6.5", + "internal-ip": "^1.1.0", + "karma": "^0.13.15", + "karma-browserstack-launcher": "^0.1.7", + "karma-chrome-launcher": "^0.2.1", + "karma-firefox-launcher": "^0.1.7", + "karma-mocha": "^0.2.1", + "karma-safari-launcher": "^0.1.1", + "karma-webpack": "^1.7.0", + "lolex": "^1.3.2", + "mocha": "^2.3.4", + "mocha-loader": "^0.7.1", + "node-fetch": "^1.3.3", + "permessage-deflate": ">=0.1.0", + "server-destroy": "^1.0.1", + "setimmediate": "^1.0.4", + "sinon": "^1.12.1", + "sinon-browser-only": "^1.12.1", + "webpack": "^1.12.6", + "webpack-dev-middleware": "^1.2.0", + "whatwg-fetch": "^0.10.0", + "wtfnode": "^0.2.1" + }, + "directories": { + "test": "test" + }, + "engines": { + "node": ">=0.8.0" + }, + "gitHead": "279aa5f20df2d08d27a60e3386b80af8ab9a5fd3", + "homepage": "https://github.com/gitterHQ/halley#readme", + "keywords": [ + "comet", + "websocket", + "pubsub", + "bayeux", + "ajax", + "http" + ], + "license": "MIT", + "main": "./index.js", + "name": "halley", + "optionalDependencies": {}, + "readme": "# Halley\n\n[![Join the chat at https://gitter.im/gitterHQ/halley](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gitterHQ/halley?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/gitterHQ/halley.svg)](https://travis-ci.org/gitterHQ/halley) [![Coverage Status](https://coveralls.io/repos/gitterHQ/halley/badge.svg?branch=master&service=github)](https://coveralls.io/github/gitterHQ/halley?branch=master)\n\nHalley is an experimental fork of James Coglan's excellent Faye library.\n\n## Differences from Faye\n\nThe main differences from Faye are (listed in no particular order):\n* **Uses promises** (and Bluebird's promise cancellation feature) to do the heavy-lifting whereever possible.\n* No Ruby client or server and no server support. Halley is a **Javascript Bayeux client only**\n* **Webpack/browserify packaging**\n* **Client reset support**. This will force the client to rehandshake. This can be useful when the application realises that the connection is dead before the bayeux client does and allows for faster recovery in these situations.\n* **No eventsource support** as we've found them to be unreliable in a ELB/haproxy setup\n* All **durations are in milliseconds**, not seconds\n* Wherever possible, implementations have been replaced with external libraries:\n * Uses [bluebird](https://github.com/petkaantonov/bluebird/) for promises\n * Uses backbone events (or backbone-events-standalone) for events\n * Mocha/sinon/karma for testing\n\n## Why's it called \"Halley\"?\n\nLots of reasons! Halley implements the Bayeux Protocol. The [Bayeux Tapestry](https://en.wikipedia.org/wiki/Bayeux_Tapestry)\ncontains the first know depiction of Halley's Comet. Halley is a [cometd](https://cometd.org) client.\n\n### Usage\n\n### Basic Example\n\n```js\nvar Halley = require('halley');\nvar client = new Halley.Client('/bayeux');\n\nfunction onMessage(message) {\n console.log('Incoming message', message);\n}\n\nclient.subscribe('/channel', onMessage);\n\nclient.publish('/channel2', { value: 1 })\n .then(function(response) {\n console.log('Publish returned', response);\n })\n .catch(function(err) {\n console.error('Publish failed:', err);\n });\n```\n\n### Advanced Example\n\n```js\nvar Halley = require('halley');\nvar Promise = require('bluebird');\n\n/** Create a client (showing the default options) */\nvar client = new Halley.Client('/bayeux', {\n /* The amount of time to wait (in ms) between successive\n * retries on a single message send */\n retry: 30000,\n\n /**\n * An integer representing the minimum period of time, in milliseconds, for a\n * client to delay subsequent requests to the /meta/connect channel.\n * A negative period indicates that the message should not be retried.\n * A client MUST implement interval support, but a client MAY exceed the\n * interval provided by the server. A client SHOULD implement a backoff\n * strategy to increase the interval if requests to the server fail without\n * new advice being received from the server.\n */\n interval: 0,\n\n /**\n * An integer representing the period of time, in milliseconds, for the\n * server to delay responses to the /meta/connect channel.\n * This value is merely informative for clients. Bayeux servers SHOULD honor\n * timeout advices sent by clients.\n */\n timeout: 30000,\n\n /**\n * The maximum number of milliseconds to wait before considering a\n * request to the Bayeux server failed.\n */\n maxNetworkDelay: 30000,\n\n /**\n * The maximum number of milliseconds to wait for a WebSocket connection to\n * be opened. It does not apply to HTTP connections.\n */\n connectTimeout: 30000,\n\n /**\n * Maximum time to wait on disconnect\n */\n disconnectTimeout: 10000\n});\n\nfunction onMessage(message) {\n console.log('Incoming message', message);\n}\n\n/*\n *`.subscribe` returns a thenable with a `.unsubscribe` method\n * but will also resolve as a promise \n */\nvar subscription = client.subscribe('/channel', onMessage);\n\nsubscription\n .then(function() {\n console.log('Subscription successful');\n })\n .catch(function(err) {\n console.log('Subscription failed: ', err);\n });\n\n/** As an example, wait 10 seconds and cancel the subscription */\nPromise.delay(10000)\n .then(function() {\n return subscription.unsubscribe();\n });\n```\n\n\n### Debugging\n\nHalley uses [debug](https://github.com/visionmedia/debug) for debugging.\n\n * To enable in nodejs, `export DEBUG=halley:*`\n * To enable in a browser, `window.localStorage.debug='halley:*'`\n\nTo limit the amount of debug logging produced, you can specify individual categories, eg `export DEBUG=halley:client`.\n\n## Tests\n\nMost of the tests in Halley are end-to-end integration tests, which means running a server environment alongside client tests which run in the browser.\n\nIn order to isolate tests from one another, the server will spawn a new Faye server and Proxy server for each test (and tear them down when the test is complete). \n\nSome of the tests connect to Faye directly, while other tests are performed via the Proxy server which is intended to simulate an reverse-proxy/ELB situation common in many production environments.\n\nThe tests do horrible things in order to test some of the situations we've discovered when using Bayeux and websockets on the web. Examples of things we test to ensure that the client recovers include:\n\n* Corrupting websocket streams, like bad MITM proxies sometimes do\n* Dropping random packets\n* Restarting the server during the test\n* Deleting the client connection from the server during the test\n* Not communicating TCP disconnects from the server-to-client and client-to-server when communicating via the proxy (a situation we've seen on ELB)\n\n## License\n\n(The MIT License)\n\nCopyright (c) 2009-2014 James Coglan and contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the 'Software'), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", + "readmeFilename": "README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/gitterHQ/halley.git" + }, + "scripts": { + "test": "gulp test" + }, + "version": "0.5.2" +} diff --git a/libs/halley/test/.eslintrc.json b/libs/halley/test/.eslintrc.json new file mode 100644 index 0000000..8397368 --- /dev/null +++ b/libs/halley/test/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "env": { + "node": true, + "browser": true, + "mocha": true + } +} diff --git a/libs/halley/test/.jshintrc b/libs/halley/test/.jshintrc new file mode 100644 index 0000000..b13e059 --- /dev/null +++ b/libs/halley/test/.jshintrc @@ -0,0 +1,12 @@ +{ + "node": true, + "unused": true, + "predef": [ + "describe", + "it", + "before", + "after", + "beforeEach", + "afterEach" + ] +} diff --git a/libs/halley/test/browser-websocket-test.js b/libs/halley/test/browser-websocket-test.js new file mode 100644 index 0000000..26bd705 --- /dev/null +++ b/libs/halley/test/browser-websocket-test.js @@ -0,0 +1,51 @@ +'use strict'; + +var WebSocket = require('../lib/transport/browser/browser-websocket'); +var uri = require('../lib/util/uri'); +var Advice = require('../lib/protocol/advice'); + +describe('browser websocket transport', function() { + beforeEach(function() { + this.dispatcher = { + handleResponse: function() { + }, + handleError: function() { + } + }; + + this.advice = new Advice({ + interval: 0, + timeout: 1000, + retry: 1 + }); + + }); + + describe('direct', function() { + + beforeEach(function() { + this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlDirect), this.advice); + }); + + afterEach(function() { + this.websocket.close(); + }); + + require('./specs/websocket-spec')(); + }); + + describe('proxied', function() { + + beforeEach(function() { + this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlProxied), this.advice); + }); + + afterEach(function() { + this.websocket.close(); + }); + + require('./specs/websocket-server-restart-spec')(); + require('./specs/websocket-bad-connection-spec')(); + }); + +}); diff --git a/libs/halley/test/channel-set-test.js b/libs/halley/test/channel-set-test.js new file mode 100644 index 0000000..0d22b0f --- /dev/null +++ b/libs/halley/test/channel-set-test.js @@ -0,0 +1,140 @@ +'use strict'; + +var ChannelSet = require('../lib/protocol/channel-set'); +var Subscription = require('../lib/protocol/subscription'); +var assert = require('assert'); +var Promise = require('bluebird'); +var sinon = require('sinon'); + +function settleAll(promises) { + return Promise.all(promises.map(function(promise) { return promise.reflect(); })); +} + +describe('channel-set', function() { + + beforeEach(function() { + this.onSubscribe = sinon.spy(function() { + return Promise.delay(1); + }); + + this.onUnsubscribe = sinon.spy(function() { + return Promise.delay(1); + }); + + this.onSubscribeBadChannel = sinon.spy(function() { + return Promise.delay(1).throw(new Error('Fail')); + }); + + this.onUnsubscribeBadChannel = sinon.spy(function() { + return Promise.delay(1).throw(new Error('Fail')); + }); + + this.sub1 = new Subscription(); + this.sub2 = new Subscription(); + this.sub3 = new Subscription(); + + this.channelSet = new ChannelSet(this.onSubscribe, this.onUnsubscribe); + this.channelSetBadChannel = new ChannelSet(this.onSubscribeBadChannel, this.onUnsubscribeBadChannel); + }); + + it('should subscribe', function() { + return this.channelSet.subscribe('/x', this.sub1) + .bind(this) + .then(function() { + assert(this.onSubscribe.calledWith('/x')); + assert(this.onSubscribe.calledOnce); + + assert.deepEqual(this.channelSet.getKeys(), ['/x']); + }); + }); + + it('should unsubscribe correctly', function() { + return this.channelSet.subscribe('/x', this.sub1) + .bind(this) + .then(function() { + assert(this.onSubscribe.calledWith('/x')); + assert(this.onSubscribe.calledOnce); + assert(this.onUnsubscribe.notCalled); + + return this.channelSet.unsubscribe('/x', this.sub1); + }) + .then(function() { + assert(this.onSubscribe.calledOnce); + assert(this.onUnsubscribe.calledOnce); + assert(this.onUnsubscribe.calledWith('/x')); + }); + }); + + + it('should serialize multiple subscribes that occur in parallel', function() { + return Promise.all([ + this.channelSet.subscribe('/x', this.sub1), + this.channelSet.subscribe('/x', this.sub2) + ]) + .bind(this) + .then(function() { + assert(this.onSubscribe.calledWith('/x')); + assert(this.onSubscribe.calledOnce); + }) + .then(function() { + assert.deepEqual(this.channelSet.getKeys(), ['/x']); + }); + }); + + it('should fail both subscriptions when subscribe occurs in parallel', function() { + var p1 = this.channelSetBadChannel.subscribe('/x', this.sub1); + var p2 = this.channelSetBadChannel.subscribe('/x', this.sub2); + + // Surpress warnings in tests: + p1.catch(function() {}); + p2.catch(function() {}); + + return settleAll([p1, p2]) + .bind(this) + .each(function(x) { + assert(x.isRejected()); + + assert(this.onSubscribeBadChannel.calledWith('/x')); + assert(this.onSubscribeBadChannel.calledTwice); + }) + .then(function() { + assert.deepEqual(this.channelSetBadChannel.getKeys(), []); + }); + }); + + it('should serialize subscribes followed by unsubscribed', function() { + return Promise.all([ + this.channelSet.subscribe('/x', this.sub1), + this.channelSet.unsubscribe('/x', this.sub1), + this.channelSet.subscribe('/x', this.sub2) + ]) + .bind(this) + .then(function() { + assert(this.onSubscribe.calledWith('/x')); + assert(this.onSubscribe.calledTwice); + + assert(this.onUnsubscribe.calledWith('/x')); + assert(this.onUnsubscribe.calledOnce); + + assert.deepEqual(this.channelSet.getKeys(), ['/x']); + }); + }); + + it('should handle parallel subscribes being cancelled', function() { + var s1 = this.channelSet.subscribe('/x', this.sub1); + var s2 = this.channelSet.subscribe('/x', this.sub2); + + s1.cancel(); + + return s2 + .bind(this) + .then(function() { + assert(s1.isCancelled()); + + assert(this.onSubscribe.calledWith('/x')); + assert(this.onSubscribe.calledTwice); + }); + + }); + +}); diff --git a/libs/halley/test/client-all-transports-test.js b/libs/halley/test/client-all-transports-test.js new file mode 100644 index 0000000..2288cb5 --- /dev/null +++ b/libs/halley/test/client-all-transports-test.js @@ -0,0 +1,73 @@ +'use strict'; + +var Halley = require('..'); +var Websocket = require('../lib/transport/base-websocket'); +var assert = require('assert'); + +describe('client-all-transport', function() { + + describe('direct', function() { + + beforeEach(function() { + this.openSocketsBefore = Websocket._countSockets(); + + this.client = new Halley.Client(this.urlDirect, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout + }); + }); + + afterEach(function() { + return this.client.disconnect() + .bind(this) + .then(function() { + // Ensure that all sockets are closed + assert.strictEqual(Websocket._countSockets(), this.openSocketsBefore); + }); + }); + + require('./specs/client-spec')(); + require('./specs/client-bad-websockets-spec')(); + }); + + describe('proxied', function() { + + beforeEach(function() { + this.client = new Halley.Client(this.urlProxied, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout + }); + }); + + afterEach(function() { + return this.client.disconnect() + .then(function() { + // Ensure that all sockets are closed + assert.strictEqual(Websocket._countSockets(), 0); + }); + }); + + require('./specs/client-proxied-spec')(); + + }); + + describe('invalid-endpoint', function() { + beforeEach(function() { + this.client = new Halley.Client(this.urlInvalid, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout + }); + }); + + afterEach(function() { + return this.client.disconnect() + .then(function() { + // Ensure that all sockets are closed + assert.strictEqual(Websocket._countSockets(), 0); + }); + }); + + require('./specs/client-invalid-endpoint-spec')(); + }); + +}); diff --git a/libs/halley/test/client-long-polling-test.js b/libs/halley/test/client-long-polling-test.js new file mode 100644 index 0000000..958aa53 --- /dev/null +++ b/libs/halley/test/client-long-polling-test.js @@ -0,0 +1,61 @@ +'use strict'; + +var Halley = require('..'); + +describe('client-long-polling', function() { + + describe('direct', function() { + + beforeEach(function() { + this.client = new Halley.Client(this.urlDirect, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout, + connectionTypes: ['long-polling'], + disabled: ['websocket', 'callback-polling'] + }); + }); + + afterEach(function() { + return this.client.disconnect(); + }); + + require('./specs/client-spec')(); + + }); + + describe('proxied', function() { + + beforeEach(function() { + this.client = new Halley.Client(this.urlProxied, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout, + connectionTypes: ['long-polling'], + disabled: ['websocket', 'callback-polling'] + }); + }); + + afterEach(function() { + return this.client.disconnect(); + }); + + require('./specs/client-proxied-spec')(); + + }); + + describe('invalid-endpoint', function() { + beforeEach(function() { + this.client = new Halley.Client(this.urlInvalid, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout, + connectionTypes: ['long-polling'], + disabled: ['websocket', 'callback-polling'] + }); + }); + + afterEach(function() { + return this.client.disconnect(); + }); + + require('./specs/client-invalid-endpoint-spec')(); + }); +}); diff --git a/libs/halley/test/client-shutdown-test.js b/libs/halley/test/client-shutdown-test.js new file mode 100644 index 0000000..a40c38e --- /dev/null +++ b/libs/halley/test/client-shutdown-test.js @@ -0,0 +1,32 @@ +'use strict'; + +var fork = require('child_process').fork; +var assert = require('assert'); + +describe('client-shutdown-test', function() { + + it('should cleanup after disconnect on subscribe', function(done) { + var testProcess = fork(__dirname + '/helpers/cleanup-test-process', [this.urlDirect], { + env: { + SUBSCRIBE: 1 + } + }); + + testProcess.on('close', function (code) { + assert.strictEqual(code, 0); + done(); + }); + + }); + + it('should cleanup after disconnect on no messages', function(done) { + var testProcess = fork(__dirname + '/helpers/cleanup-test-process', [this.urlDirect]); + + testProcess.on('close', function (code) { + assert.strictEqual(code, 0); + done(); + }); + + }); + +}); diff --git a/libs/halley/test/client-websockets-test.js b/libs/halley/test/client-websockets-test.js new file mode 100644 index 0000000..45c2e8b --- /dev/null +++ b/libs/halley/test/client-websockets-test.js @@ -0,0 +1,78 @@ +'use strict'; + +var Halley = require('..'); +var Websocket = require('../lib/transport/base-websocket'); +var assert = require('assert'); + +describe('client-websocket', function() { + describe('direct', function() { + beforeEach(function() { + this.openSocketsBefore = Websocket._countSockets(); + + this.client = new Halley.Client(this.urlDirect, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout, + connectionTypes: ['websocket'], + disabled: ['long-polling', 'callback-polling'] + }); + }); + + afterEach(function() { + return this.client.disconnect() + .bind(this) + .then(function() { + // Ensure that all sockets are closed + assert.strictEqual(Websocket._countSockets(), this.openSocketsBefore); + }); + + }); + + require('./specs/client-spec')(); + }); + + describe('proxied', function() { + beforeEach(function() { + this.client = new Halley.Client(this.urlProxied, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout, + + connectionTypes: ['websocket'], + disabled: ['long-polling', 'callback-polling'] + }); + }); + + afterEach(function() { + return this.client.disconnect() + .then(function() { + // Ensure that all sockets are closed + assert.strictEqual(Websocket._countSockets(), 0); + }); + + }); + + require('./specs/client-proxied-spec')(); + }); + + describe('invalid-endpoint', function() { + beforeEach(function() { + this.client = new Halley.Client(this.urlInvalid, { + retry: this.clientOptions.retry, + timeout: this.clientOptions.timeout, + + connectionTypes: ['websocket'], + disabled: ['long-polling', 'callback-polling'] + }); + }); + + afterEach(function() { + return this.client.disconnect() + .then(function() { + // Ensure that all sockets are closed + assert.strictEqual(Websocket._countSockets(), 0); + }); + }); + + require('./specs/client-invalid-endpoint-spec')(); + }); + +}); diff --git a/libs/halley/test/errors-test.js b/libs/halley/test/errors-test.js new file mode 100644 index 0000000..7ee0aba --- /dev/null +++ b/libs/halley/test/errors-test.js @@ -0,0 +1,27 @@ +'use strict'; + +var errors = require('../lib/util/errors'); +var Promise = require('bluebird'); +var assert = require('assert'); + +describe('errors', function() { + + it('TransportError should work with bluebird', function() { + return Promise.reject(new errors.TransportError()) + .catch(errors.TransportError, errors.BayeuxError, function() { + }) + .catch(function() { + assert.ok(false); + }); + }); + + it('BayeuxError should work with bluebird', function() { + return Promise.reject(new errors.BayeuxError()) + .catch(errors.TransportError, errors.BayeuxError, function() { + }) + .catch(function() { + assert.ok(false); + }); + }); + +}); diff --git a/libs/halley/test/extensions-test.js b/libs/halley/test/extensions-test.js new file mode 100644 index 0000000..e79028a --- /dev/null +++ b/libs/halley/test/extensions-test.js @@ -0,0 +1,39 @@ +'use strict'; + +var Extensions = require('../lib/protocol/extensions'); +var assert = require('assert'); + +describe('extensions', function() { + + it('should call extensions in sequence', function() { + var count = 0; + + function Ext(x) { + this.incoming = function(message, callback) { + assert.strictEqual(count, 0); + assert.strictEqual(message.count, x - 1); + count++; + + setTimeout(function() { + count--; + message.count = x; + callback(message); + }, 2); + } + } + var extensions = new Extensions(); + + extensions.add(new Ext(1)); + extensions.add(new Ext(2)); + extensions.add(new Ext(3)); + extensions.add(new Ext(4)); + extensions.add(new Ext(5)); + extensions.add(new Ext(6)); + + return extensions.pipe('incoming', { count: 0 }) + .then(function(message) { + assert.deepEqual(message, { count: 6 }); + }); + }); + +}); diff --git a/libs/halley/test/helpers/bayeux-server.js b/libs/halley/test/helpers/bayeux-server.js new file mode 100644 index 0000000..f5c791d --- /dev/null +++ b/libs/halley/test/helpers/bayeux-server.js @@ -0,0 +1,191 @@ +/* jshint node:true */ +'use strict'; + +var http = require('http'); +var faye = require('gitter-faye'); +var debug = require('debug')('halley:test:bayeux-server'); +var enableDestroy = require('server-destroy'); +var extend = require('lodash/object/extend'); + +function BayeuxServer() { + this.port = 0; +} + +BayeuxServer.prototype = { + + start: function(callback) { + var server = this.server = http.createServer(); + enableDestroy(server); + + var bayeux = this.bayeux = new faye.NodeAdapter({ + mount: '/bayeux', + timeout: 5, + ping: 2, + engine: { + interval: 0.3 + } + }); + bayeux.attach(server); + + this.publishTimer = setInterval(function() { + bayeux.getClient().publish('/datetime', { date: Date.now() }); + bayeux.getClient().publish('/slow', { date: Date.now() }); + }, 100); + + server.on('upgrade', function(req) { + if (self.crushWebsocketConnections) { + // Really mess things up + req.socket.write(''); + req.socket.destroy(); + } + }); + + var self = this; + + bayeux.addExtension({ + incoming: function(message, req, callback) { + var clientId = message.clientId; + if (!clientId) return callback(message); + + // This is a bit of a hack, but Faye doesn't appear to do it + // automatically: check that the client actually exists. If it + // doesn't reject it + bayeux._server._engine.clientExists(clientId, function(exists) { + if(!exists) { + message.error = '401:' + clientId + ':Unknown client'; + } + + return callback(message); + }); + }, + + outgoing: function(message, req, callback) { + if (message.successful === false && message.error) { + // If we're sending 401 messages to the client, they don't + // actually have a connection, so tell them to rehandshake + if (message.error.indexOf('401:') === 0) { + message.advice = extend(message.advice || {}, { "reconnect": "handshake", interval: 0 }); + } + } + return callback(message); + } + }); + + bayeux.addExtension({ + incoming: function(message, req, callback) { + + if (message.channel === '/meta/subscribe' && message.subscription === '/slow') { + return setTimeout(function() { + callback(message); + }, 200); + } + + return callback(message); + } + + }); + + bayeux.addExtension({ + incoming: function(message, req, callback) { + + if (message.channel === '/meta/handshake' && message.ext && message.ext.deny) { + message.error = '401::Unauthorised'; + } + + return callback(message); + }, + + outgoing: function(message, req, callback) { + if (message.channel === '/meta/handshake' && message.error === '401::Unauthorised') { + message.advice = { reconnect: 'none' }; + } + + return callback(message); + } + + }); + + bayeux.addExtension({ + incoming: function(message, req, callback) { + if (self.crushWebsocketConnections) { + if (req && req.headers.connection === 'Upgrade') { + debug('Disconnecting websocket'); + req.socket.destroy(); + return; + } + } + + if (message.channel === '/meta/subscribe' && message.subscription === '/banned') { + message.error = 'Invalid subscription'; + } + + if (message.channel === '/devnull') { + return; + } + + if (message.channel === '/meta/handshake') { + if (message.ext && message.ext.failHandshake) { + message.error = 'Unable to handshake'; + } + } + + callback(message); + }, + + outgoing: function(message, req, callback) { + var advice; + if (message.channel === '/advice-retry') { + advice = message.advice = message.advice || {}; + advice.reconnect = 'retry'; + advice.timeout = 2000; + } + + if (message.channel === '/advice-handshake') { + advice = message.advice = message.advice || {}; + advice.reconnect = 'handshake'; + advice.interval = 150; + // advice.timeout = 150; + } + + if (message.channel === '/advice-none') { + advice = message.advice = message.advice || {}; + advice.reconnect = 'none'; + } + + return callback(message); + } + }); + + server.listen(this.port, function(err) { + if (err) return callback(err); + self.port = server.address().port; + callback(null, server.address().port); + }); + }, + + stop: function(callback) { + clearTimeout(this.publishTimer); + clearTimeout(this.uncrushTimeout); + this.server.destroy(callback); + this.server = null; + }, + + deleteClient: function(clientId, callback) { + debug('Deleting client', clientId); + this.bayeux._server._engine.destroyClient(clientId, callback); + }, + + crush: function(timeout) { + if (this.crushWebsocketConnections) return; + this.crushWebsocketConnections = true; + this.uncrushTimeout = setTimeout(this.uncrush.bind(this), timeout || 5000); + }, + + uncrush: function() { + if (!this.crushWebsocketConnections) return; + this.crushWebsocketConnections = false; + clearTimeout(this.uncrushTimeout); + } +}; + +module.exports = BayeuxServer; diff --git a/libs/halley/test/helpers/bayeux-with-proxy-server.js b/libs/halley/test/helpers/bayeux-with-proxy-server.js new file mode 100644 index 0000000..158a6b4 --- /dev/null +++ b/libs/halley/test/helpers/bayeux-with-proxy-server.js @@ -0,0 +1,73 @@ +/* jshint node:true */ +'use strict'; + +var BayeuxServer = require('./bayeux-server'); +var ProxyServer = require('./proxy-server'); +var Promise = require('bluebird'); + +function BayeuxWithProxyServer(localIp) { + this.localIp = localIp; +} + +BayeuxWithProxyServer.prototype = { + start: function(callback) { + var self = this; + this.bayeuxServer = new BayeuxServer(); + this.bayeuxServer.start(function(err, bayeuxPort) { + if (err) return callback(err); + self.proxyServer = new ProxyServer(bayeuxPort); + self.proxyServer.start(function(err, proxyPort) { + if (err) return callback(err); + + return callback(null, { + bayeux: 'http://' + self.localIp + ':' + bayeuxPort + '/bayeux', + proxied: 'http://' + self.localIp + ':' + proxyPort + '/bayeux', + }); + }); + }); + }, + + stop: function(callback) { + var self = this; + + this.proxyServer.stop(function(err) { + + if (err) return callback(err); + self.bayeuxServer.stop(function(err) { + + if (err) return callback(err); + callback(); + }); + }); + + }, + + networkOutage: Promise.method(function(timeout) { + this.proxyServer.disableTraffic(timeout); + }), + + stopWebsockets: Promise.method(function(timeout) { + this.bayeuxServer.crush(timeout); + }), + + deleteSocket: Promise.method(function(clientId) { + return Promise.fromCallback(this.bayeuxServer.deleteClient.bind(this.bayeuxServer, clientId)); + }), + + restart: Promise.method(function() { + var proxy = this.proxyServer; + return Promise.fromCallback(proxy.stop.bind(proxy)) + .delay(500) + .then(function() { + return Promise.fromCallback(proxy.start.bind(proxy)); + }); + }), + + restoreAll: Promise.method(function() { + this.proxyServer.enableTraffic(); + this.bayeuxServer.uncrush(); + }), + +}; + +module.exports = BayeuxWithProxyServer; diff --git a/libs/halley/test/helpers/cleanup-test-process.js b/libs/halley/test/helpers/cleanup-test-process.js new file mode 100644 index 0000000..2abcbfa --- /dev/null +++ b/libs/halley/test/helpers/cleanup-test-process.js @@ -0,0 +1,49 @@ +'use strict'; + +var wtf = require('wtfnode'); // Must be first +var Halley = require('../..'); +var Promise = require('bluebird'); + +var url = process.argv[2]; + +var client = new Halley.Client(url); + +function doSubscribe() { + return client.publish('/channel', { data: 1 }) + .then(function() { + var resolve; + var gotMessage = new Promise(function(res) { + resolve = res; + }); + + return [gotMessage, client.subscribe('/datetime', function() { + resolve(); + })]; + }) + .spread(function(message, subscription) { + return subscription.unsubscribe(); + }); +} + +function doNoSubscribe() { + return client.connect() + .then(function() { + return Promise.delay(client._advice.timeout + 1000); + }); +} + +(process.env.SUBSCRIBE ? doSubscribe() : doNoSubscribe()) + .then(function() { + return client.disconnect(); + }) + .then(function() { + client = null; + setInterval(function() { + wtf.dump(); + }, 1000).unref(); + }) + .catch(function(err) { + console.error(err && err.stack || err); + process.exit(1); + }) + .done(); diff --git a/libs/halley/test/helpers/proxy-server.js b/libs/halley/test/helpers/proxy-server.js new file mode 100644 index 0000000..644259c --- /dev/null +++ b/libs/halley/test/helpers/proxy-server.js @@ -0,0 +1,123 @@ +'use strict'; + +var net = require('net'); +var debug = require('debug')('halley:test:proxy-server'); +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var enableDestroy = require('server-destroy'); + +function ProxyServer(serverPort) { + EventEmitter.call(this); + this.setMaxListeners(100); + this.serverPort = serverPort; + this.listenPort = 0; +} +util.inherits(ProxyServer, EventEmitter); + +ProxyServer.prototype.start = function(callback) { + var self = this; + var server = this.server = net.createServer(function(c) { //'connection' listener + debug('client connected'); + c.on('end', function() { + debug('client disconnected'); + }); + + self.createClient(c); + }); + + enableDestroy(server); + + server.listen(this.listenPort, function() { //'listening' listener + debug('server bound'); + + self.listenPort = server.address().port; + callback(null, server.address().port); + }); +}; + +ProxyServer.prototype.stop = function(callback) { + this.emit('close'); + this.server.destroy(callback); +}; + +ProxyServer.prototype.createClient = function(incoming) { + var self = this; + debug('connection created'); + + var backend = net.connect({ port: this.serverPort }, function() { + debug('backend connection created'); + + var onClose = function() { + self.removeListener('close', onClose); + incoming.destroy(); + backend.destroy(); + }.bind(this); + + self.on('close', onClose); + + incoming.on('data', function(data) { + if (self.trafficDisabled) { + debug('dropping incoming request'); + return; + } + + backend.write(data); + }); + + backend.on('data', function(data) { + if (self.trafficDisabled) { + debug('dropping backend response'); + return; + } + + incoming.write(data); + }); + + incoming.on('end', function() { + debug('incoming end'); + // Intentionally leave sockets hanging + }); + + backend.on('end', function() { + debug('backend end'); + // Intentionally leave sockets hanging + incoming.destroy(); + }); + + incoming.on('error', function() { + debug('incoming error'); + backend.destroy(); + }); + + backend.on('error', function() { + debug('backend error'); + }); + + backend.on('close', function() { + debug('backend close'); + incoming.destroy(); + }); + + incoming.on('close', function() { + debug('incoming close'); + }); + + }); +}; + +ProxyServer.prototype.disableTraffic = function(timeout) { + debug('Trashing all incoming traffic'); + clearTimeout(this.outageTimer); + this.outageTimer = setTimeout(this.enableTraffic.bind(this), timeout || 5000); + this.trafficDisabled = true; +}; + +ProxyServer.prototype.enableTraffic = function() { + clearTimeout(this.outageTimer); + debug('Re-enabling incoming traffic'); + this.trafficDisabled = false; +}; + + + +module.exports = ProxyServer; diff --git a/libs/halley/test/helpers/public/index.html b/libs/halley/test/helpers/public/index.html new file mode 100644 index 0000000..ee76fb1 --- /dev/null +++ b/libs/halley/test/helpers/public/index.html @@ -0,0 +1,13 @@ + + + + + Halley Tests + + + +
+
+ + + diff --git a/libs/halley/test/helpers/remote-server-control.js b/libs/halley/test/helpers/remote-server-control.js new file mode 100644 index 0000000..224a3af --- /dev/null +++ b/libs/halley/test/helpers/remote-server-control.js @@ -0,0 +1,83 @@ +/* jshint browser:true */ + +'use strict'; + +var Promise = require('bluebird'); + +if (!window.Promise) { + window.Promise = Promise; +} + +// Polyfill if required +require('whatwg-fetch'); + +function fetchBluebird(url, options) { + return Promise.resolve(window.fetch(url, options)); +} + +function RemoteServerControl() { + this.urlRoot = HALLEY_TEST_SERVER; + this.id = null; +} + +RemoteServerControl.prototype = { + setup: function() { + return fetchBluebird(this.urlRoot + '/setup', { + method: 'post', + body: "" + }) + .bind(this) + .then(function(response) { + return Promise.resolve(response.json()); + }) + .then(function(json) { + this.id = json.id; + return json.urls; + }); + }, + + teardown: function() { + return fetchBluebird(this.urlRoot + '/control/' + this.id + '/teardown', { + method: 'post', + body: "" + }); + }, + + networkOutage: function(timeout) { + return fetchBluebird(this.urlRoot + '/control/' + this.id + '/network-outage?timeout=' + (timeout || 5000), { + method: 'post', + body: "" + }); + }, + + stopWebsockets: function(timeout) { + return fetchBluebird(this.urlRoot + '/control/' + this.id + '/stop-websockets?timeout=' + (timeout || 5000), { + method: 'post', + body: "" + }); + }, + + deleteSocket: function(clientId) { + return fetchBluebird(this.urlRoot + '/control/' + this.id + '/delete/' + clientId, { + method: 'post', + body: "" + }); + }, + + restart: function() { + return fetchBluebird(this.urlRoot + '/control/' + this.id + '/restart', { + method: 'post', + body: "" + }); + }, + + restoreAll: function() { + return fetchBluebird(this.urlRoot + '/control/' + this.id + '/restore-all', { + method: 'post', + body: "" + }); + }, + +}; + +module.exports = RemoteServerControl; diff --git a/libs/halley/test/helpers/server.js b/libs/halley/test/helpers/server.js new file mode 100644 index 0000000..3a7dff0 --- /dev/null +++ b/libs/halley/test/helpers/server.js @@ -0,0 +1,208 @@ +/* jshint node:true */ +'use strict'; + +var http = require('http'); +var express = require('express'); +var webpack = require('webpack'); +var webpackMiddleware = require("webpack-dev-middleware"); +var PUBLIC_DIR = __dirname + '/public'; +var debug = require('debug')('halley:test:server'); +var BayeuxWithProxyServer = require('./bayeux-with-proxy-server'); +var internalIp = require('internal-ip'); +var enableDestroy = require('server-destroy'); + +var localIp = process.env.HALLEY_LOCAL_IP || internalIp.v4(); + +var idCounter = 0; +var servers = {}; +var server; + +var port = process.env.PORT || '8000'; + +function listen(options, callback) { + var app = express(); + server = http.createServer(app); + enableDestroy(server); + + if (options.webpack) { + app.use(webpackMiddleware(webpack({ + context: __dirname + "/..", + entry: "mocha!./test-suite-browser", + output: { + path: __dirname + "/", + filename: "test-suite-browser.js" + }, + resolve: { + alias: { + sinon: 'sinon-browser-only' + } + }, + module: { + noParse: [ + /sinon-browser-only/ + ] + }, + devtool: "#eval", + node: { + console: false, + global: true, + process: true, + Buffer: false, + __filename: false, + __dirname: false, + setImmediate: false + }, + plugins:[ + new webpack.DefinePlugin({ + HALLEY_TEST_SERVER: JSON.stringify('http://' + localIp + ':' + port) + }) + ] + + }), { + noInfo: false, + quiet: false, + + watchOptions: { + aggregateTimeout: 300, + poll: true + }, + + publicPath: "/", + stats: { colors: true } + })); + + app.use(express.static(PUBLIC_DIR)); + } + + app.all('/*', function(req, res, next) { + res.set('Access-Control-Allow-Origin', '*'); + next(); + }); + + app.post('/setup', function(req, res, next) { + var server = new BayeuxWithProxyServer(localIp); + var id = idCounter++; + servers[id] = server; + server.start(function(err, urls) { + if (err) return next(err); + + res.send({ id: id, urls: urls }); + }); + }); + + app.post('/control/:id/teardown', function(req, res, next) { + var id = req.params.id; + + var server = servers[id]; + delete servers[id]; + + server.stop(function(err) { + if (err) return next(err); + res.send('OK'); + }); + }); + + app.post('/control/:id/delete/:clientId', function(req, res, next) { + var id = req.params.id; + var clientId = req.params.clientId; + var server = servers[id]; + + server.deleteSocket(clientId) + .then(function() { + res.send('OK'); + }) + .catch(next); + }); + + + app.post('/control/:id/network-outage', function(req, res, next) { + var id = req.params.id; + var server = servers[id]; + + server.networkOutage() + .then(function() { + res.send('OK'); + }) + .catch(next); + }); + + app.post('/control/:id/restore-all', function(req, res, next) { + var id = req.params.id; + var server = servers[id]; + + server.restoreAll() + .then(function() { + res.send('OK'); + }) + .catch(next); + }); + + app.post('/control/:id/restart', function(req, res, next) { + var id = req.params.id; + var server = servers[id]; + + server.restart() + .then(function() { + res.send('OK'); + }) + .catch(next); + }); + + app.post('/control/:id/stop-websockets', function(req, res, next) { + var id = req.params.id; + var server = servers[id]; + + server.stopWebsockets() + .then(function() { + res.send('OK'); + }) + .catch(next); + }); + + app.use(function(err, req, res, next) { // jshint ignore:line + res.status(500).send(err.message); + }); + + app.get('*', function(req, res) { + res.status(404).send('Not found'); + }); + + server.listen(port, callback); +} + +function unlisten(callback) { + debug('Unlisten'); + Object.keys(servers).forEach(function(id) { + var server = servers[id]; + + server.stop(function(err) { + if (err) { + debug('Error stopping server ' + err); + } + }); + + }); + + servers = {}; + server.destroy(callback); +} + +exports.listen = listen; +exports.unlisten = unlisten; + +if (require.main === module) { + process.on('uncaughtException', function(e) { + console.error(e.stack || e); + process.exit(1); + }); + + listen({ webpack: true }, function(err) { + + if (err) { + debug('Unable to start server: %s', err); + return; + } + + debug('Listening'); + }); +} diff --git a/libs/halley/test/node-websocket-test.js b/libs/halley/test/node-websocket-test.js new file mode 100644 index 0000000..7acc1f3 --- /dev/null +++ b/libs/halley/test/node-websocket-test.js @@ -0,0 +1,52 @@ +'use strict'; + +var WebSocket = require('../lib/transport/node/node-websocket'); +var uri = require('../lib/util/uri'); +var Advice = require('../lib/protocol/advice'); + +describe('node websocket transport', function() { + + beforeEach(function() { + this.dispatcher = { + handleResponse: function() { + }, + handleError: function() { + } + }; + + this.advice = new Advice({ + interval: 0, + timeout: 1000, + retry: 1 + }); + + }); + + describe('direct', function() { + + beforeEach(function() { + this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlDirect), this.advice); + }); + + afterEach(function() { + this.websocket.close(); + }); + + require('./specs/websocket-spec')(); + }); + + describe('proxied', function() { + + beforeEach(function() { + this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlProxied), this.advice); + }); + + afterEach(function() { + this.websocket.close(); + }); + + require('./specs/websocket-server-restart-spec')(); + require('./specs/websocket-bad-connection-spec')(); + }); + +}); diff --git a/libs/halley/test/on-before-unload-test.js b/libs/halley/test/on-before-unload-test.js new file mode 100644 index 0000000..8373bde --- /dev/null +++ b/libs/halley/test/on-before-unload-test.js @@ -0,0 +1,34 @@ +'use strict'; + +var Halley = require('..'); +var globalEvents = require('../lib/util/global-events'); + +describe('onbeforeunload', function() { + var client; + + beforeEach(function() { + client = new Halley.Client(this.urlDirect, { timeout: 45 }); + }); + + afterEach(function() { + client.disconnect(); + }); + + it('should respond to beforeunload correctly', function(done) { + var count = 0; + var subscription = client.subscribe('/datetime', function() { + count++; + + if (count === 3) { + client.on('disconnect', done); + globalEvents.trigger('beforeunload'); + } + }); + + subscription.catch(function(err) { + done(err); + }); + + }); + +}); diff --git a/libs/halley/test/promise-util-test.js b/libs/halley/test/promise-util-test.js new file mode 100644 index 0000000..9f6091b --- /dev/null +++ b/libs/halley/test/promise-util-test.js @@ -0,0 +1,625 @@ +'use strict'; + +var promiseUtil = require('../lib/util/promise-util'); +var assert = require('assert'); +var Promise = require('bluebird'); + +describe('promise-util', function() { + describe('Synchronized', function() { + + beforeEach(function() { + this.sync = new promiseUtil.Synchronized(); + }); + + it('should synchronize access with a single item', function() { + return this.sync.sync('1', function() { + return 'a'; + }) + .bind(this) + .then(function(result) { + assert.deepEqual(this.sync._keys, {}); + assert.strictEqual(result, 'a'); + }); + }); + + it('should propogate rejections', function() { + return this.sync.sync('1', function() { + throw new Error('Crash'); + }) + .bind(this) + .then(function() { + assert.ok('Expected failure'); + }, function(err) { + assert.strictEqual(err.message, 'Crash'); + }); + }); + + it('should propogate on queued items', function() { + this.sync.sync('1', function() { return Promise.delay(1).return('a'); }); + return this.sync.sync('1', function() { + return Promise.reject(new Error('Queued error')); + }) + .bind(this) + .then(function() { + assert.ok(false, 'Expected exception'); + }, function(err) { + assert.strictEqual(err.message, 'Queued error'); + }) + .then(function() { + assert.deepEqual(this.sync._keys, {}); + }); + }); + + it('should synchronize access with multiple items', function() { + var count = 0; + return Promise.all([ + this.sync.sync('1', function() { assert.strictEqual(count++, 0); return Promise.delay(2).return('a'); }), + this.sync.sync('1', function() { assert.strictEqual(count++, 1); return 'b'; }) + ]) + .bind(this) + .then(function(result) { + assert.strictEqual(count, 2); + assert.deepEqual(this.sync._keys, {}); + assert.deepEqual(result, ['a', 'b']); + }); + }); + + it('upstream rejections should be isolated', function() { + var count = 0; + + this.sync.sync('1', function() { + return Promise.reject(new Error('Random')); + }).catch(function(err) { + assert(err.message, 'Random'); + count++; + }); + + return this.sync.sync('1', function() { return 'b'; }) + .bind(this) + .then(function(result) { + assert.strictEqual(count, 1); + + assert.deepEqual(this.sync._keys, {}); + assert.deepEqual(result, 'b'); + }); + }); + + it('cancellation should work', function() { + var count = 0; + + var p = this.sync.sync('1', function() { + return new Promise(function(resolve, reject, onCancel) { + Promise.delay(1).then(resolve); + + onCancel(function() { + count++; + }); + }); + }); + + p.cancel(); + + return Promise.delay(2) + .then(function() { + assert.strictEqual(count, 1); + }); + }); + + + it('upstream cancellations should be isolated', function() { + var p1 = this.sync.sync('1', function() { return Promise.delay(3).return('a'); }); + var p2 = this.sync.sync('1', function() { return 'b'; }); + return Promise.delay(1) + .bind(this) + .then(function() { + p1.cancel(); + return p2; + }) + .then(function(result) { + assert.deepEqual(result, 'b'); + assert.deepEqual(this.sync._keys, {}); + }); + }); + + }); + + describe('cancelBarrier', function() { + + it('should propogate resolve', function() { + return promiseUtil.cancelBarrier(Promise.resolve('a')) + .then(function(result) { + assert.strictEqual(result, 'a'); + }); + }); + + it('should propogate reject', function() { + var e = new Error(); + return promiseUtil.cancelBarrier(Promise.reject(e)) + .then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err, e); + }); + }); + + it('should prevent cancellations from propogating past the barrier', function() { + var count = 0; + var resolve; + var p1 = new Promise(function(res, rej, onCancel) { + resolve = res; + onCancel(function() { + count++; + }); + }); + + var p2 = promiseUtil.cancelBarrier(p1) + .then(function(x) { + return x; + }); + + p2.cancel(); + resolve('a'); + return p1 + .then(function(result) { + assert.strictEqual(result, 'a'); + }); + }); + + }); + + describe('after', function() { + + it('should execute after resolve', function() { + return promiseUtil.after(Promise.resolve('a')); + }); + + it('should execute after reject', function() { + var e = new Error(); + var p = Promise.delay(1).throw(e); + p.catch(function() { + // Dangling catch to prevent bluebird warnings + }); + + return promiseUtil.after(p); + }); + + it('should propogate when the source promise is cancelled', function() { + var count = 0; + var resolve; + var p1 = new Promise(function(res, rej, onCancel) { + resolve = res; + onCancel(function() { + count++; + }); + }); + + var p2 = promiseUtil.after(p1) + .then(function() { + assert.strictEqual(count, 1); + }); + + p1.cancel(); + + return p2; + }); + + + it('should execute in sequence', function() { + var count = 0; + var p1 = Promise.resolve('a'); + var p2 = promiseUtil.after(p1).then(function() { assert.strictEqual(count, 0); count++; }); + var p3 = promiseUtil.after(p2).then(function() { assert.strictEqual(count, 1); count++; }); + var p4 = promiseUtil.after(p3).then(function() { assert.strictEqual(count, 2); count++; }); + return p4.then(function() { + assert.strictEqual(count, 3); + }); + }); + }); + + describe('LazySingleton', function() { + + beforeEach(function() { + this.count = 0; + this.lazySingleton = new promiseUtil.LazySingleton(function() { + this.count++; + return this.singletonValue; + }.bind(this)); + + }); + + it('should return a value', function() { + this.singletonValue = Promise.resolve('a'); + return this.lazySingleton.get() + .bind(this) + .then(function(a) { + assert.strictEqual(this.count, 1); + assert.strictEqual(a, 'a'); + }); + }); + + it('should cache the results', function() { + this.singletonValue = Promise.resolve('a'); + return this.lazySingleton.get() + .bind(this) + .then(function(a) { + assert.strictEqual(this.count, 1); + assert.strictEqual(a, 'a'); + return this.lazySingleton.get(); + }) + .then(function(a) { + assert.strictEqual(this.count, 1); + assert.strictEqual(a, 'a'); + }); + }); + + it('should not make multiple calls', function() { + this.singletonValue = Promise.delay(1).return('a'); + return Promise.all([ + this.lazySingleton.get(), + this.lazySingleton.get() + ]) + .bind(this) + .then(function(a) { + assert.strictEqual(this.count, 1); + assert.deepEqual(a, ['a', 'a']); + }); + }); + + it('should handle cancellations', function() { + this.singletonValue = Promise.delay(10).return('a'); + + return Promise.delay(0) + .bind(this) + .then(function() { + assert(this.singletonValue.isPending()); + this.singletonValue.cancel(); + return promiseUtil.after(this.lazySingleton.get()); + }) + .then(function() { + assert.strictEqual(this.count, 1); + + this.singletonValue = Promise.delay(1).return('a'); + return this.lazySingleton.get(); + }) + .then(function(a) { + assert.strictEqual(this.count, 2); + assert.strictEqual(a, 'a'); + }); + + }); + + }); + + describe('Throttle', function() { + beforeEach(function() { + this.count = 0; + this.throwError = false; + this.throttle = new promiseUtil.Throttle(function() { + this.count++; + if (this.throwError) throw new Error('Fail'); + }.bind(this), 10); + + this.slowThrottle = new promiseUtil.Throttle(function() { + this.count++; + }.bind(this), 1000000); + }); + + it('should throttle calls', function() { + return Promise.all([ + this.throttle.fire(), + this.throttle.fire(), + this.throttle.fire() + ]) + .bind(this) + .then(function() { + assert.strictEqual(this.count, 1); + }); + }); + + it('should respect fireImmediate', function() { + return Promise.all([ + this.throttle.fire(), + this.throttle.fire(true), + Promise.delay(10).bind(this).then(function() { + return this.throttle.fire(); + }) + ]) + .bind(this) + .then(function() { + assert.strictEqual(this.count, 2); + }); + }); + + it('should not wait if fireImmediate is called on the first call', function() { + return this.slowThrottle.fire(true) + .bind(this) + .then(function() { + assert.strictEqual(this.count, 1); + }); + }); + + it('should reject on destroy', function() { + var p = this.slowThrottle.fire(); + var e = new Error(); + this.slowThrottle.destroy(e); + return p.then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err, e); + }); + }); + + it('should handle cancellations', function() { + var p = this.throttle.fire(); + return Promise.delay(1) + .bind(this) + .then(function() { + + assert(p.isCancellable()); + p.cancel(); + return Promise.delay(15); + }) + .then(function() { + assert.strictEqual(this.count, 0); + }); + }); + + it('should isolate cancels from one another', function() { + var p = this.throttle.fire(); + var p2 = this.throttle.fire(); + + return Promise.delay(1) + .bind(this) + .then(function() { + assert(p.isCancellable()); + p.cancel(); + return p2; + }) + .then(function() { + assert.strictEqual(this.count, 1); + }); + }); + + it('should cancel the trigger when all fires are cancelled', function() { + var p = this.throttle.fire(); + var p2 = this.throttle.fire(); + + return Promise.delay(1) + .bind(this) + .then(function() { + assert(p.isCancellable()); + assert(p2.isCancellable()); + p.cancel(); + p2.cancel(); + return Promise.delay(15); + }) + .then(function() { + assert.strictEqual(this.count, 0); + }); + }); + + it('should handle rejections', function() { + this.throwError = true; + return this.throttle.fire() + .bind(this) + .then(function() { + assert.ok(false, 'Expected error'); + }, function(err) { + assert.strictEqual(err.message, 'Fail'); + }); + }); + }); + + describe('Batcher', function() { + + beforeEach(function() { + this.count = 0; + this.batcher = new promiseUtil.Batcher(function(items) { + this.count++; + this.items = items; + return 'Hello'; + }.bind(this), 10); + }); + + it('should call on add', function() { + return this.batcher.add(10) + .then(function(value) { + assert.strictEqual(value, 'Hello'); + }); + }); + + it('should call on add with multiple items', function() { + return Promise.all([ + this.batcher.add(1), + this.batcher.add(2), + ]) + .spread(function(a,b) { + assert.strictEqual(a, 'Hello'); + assert.strictEqual(b, 'Hello'); + }); + }); + + it('should return a value', function() { + var p1 = this.batcher.add(1); + var p2 = this.batcher.add(2); + var p3 = this.batcher.add(3); + + assert(p1.isCancellable()); + p1.cancel(); + + return this.batcher.next() + .bind(this) + .then(function() { + assert(p1.isCancelled()); + assert.strictEqual(this.count, 1); + assert.deepEqual(this.items, [2, 3]); + return Promise.all([p2, p3]); + }) + .spread(function(a,b) { + assert.strictEqual(a, 'Hello'); + assert.strictEqual(b, 'Hello'); + }); + }); + + it('should not batch if all the items are cancelled', function() { + var p1 = this.batcher.add(1); + var p2 = this.batcher.add(2); + p1.cancel(); + p2.cancel(); + + return Promise.delay(15) + .bind(this) + .then(function() { + assert.strictEqual(this.count, 0); + }); + }); + + }); + + + describe('Sequencer', function() { + + beforeEach(function() { + this.inPlay = 0; + this.count = 0; + this.sequencer = new promiseUtil.Sequencer(); + this.fn = function() { + this.inPlay++; + this.count++; + var count = this.count; + assert.strictEqual(this.inPlay, 1); + return Promise.delay(1) + .bind(this) + .then(function() { + this.inPlay--; + assert.strictEqual(this.inPlay, 0); + return count; + }); + }.bind(this); + + this.fnReject = function() { + this.count++; + return Promise.delay(1) + .bind(this) + .then(function() { + throw new Error('Fail'); + }); + }.bind(this); + + this.fnWillBeCancelled = function() { + this.count++; + var promise = new Promise(function() {}); + + Promise.delay(1) + .then(function() { + promise.cancel(); + }); + + return promise; + }.bind(this); + + }); + + it('should sequence multiple calls', function() { + return Promise.all([ + this.sequencer.chain(this.fn), + this.sequencer.chain(this.fn) + ]) + .bind(this) + .spread(function(a, b) { + assert.strictEqual(a, 1); + assert.strictEqual(b, 2); + assert.strictEqual(this.count, 2); + }); + }); + + it('should handle rejections', function() { + var promises = [this.sequencer.chain(this.fnReject), this.sequencer.chain(this.fn)]; + return Promise.all(promises.map(function(promise) { + return promise.reflect(); + })) + .bind(this) + .spread(function(a, b) { + assert(a.isRejected()); + + assert.strictEqual(a.reason().message, 'Fail'); + + assert(b.isFulfilled()); + assert.strictEqual(b.value(), 2); + assert.strictEqual(this.count, 2); + return a; + }); + }); + + + it('should handle upstream cancellations', function() { + var p1 = this.sequencer.chain(this.fnWillBeCancelled); + var p2 = this.sequencer.chain(this.fn); + + return p2.bind(this) + .then(function(value) { + assert(p1.isCancelled()); + + assert(p2.isFulfilled()); + assert.strictEqual(value, 2); + assert.strictEqual(this.count, 2); + }); + }); + + it('should handle downstream cancellations', function() { + var count = 0; + + var p1 = this.sequencer.chain(function() { + return Promise.delay(1).then(function() { + count++; + assert.ok(false); + }); + }); + + var p2 = this.sequencer.chain(function() { + assert.strictEqual(count, 0); + }); + + p1.cancel(); + return p2; + }); + + it('should handle the queue being cleared', function() { + var count = 0; + var p1 = this.sequencer.chain(function() { + return Promise.delay(1).then(function() { + count++; + return "a"; + }); + }); + + var p2 = this.sequencer.chain(function() { + return Promise.delay(1).then(function() { + count++; + }); + }); + + p2.catch(function() {}); // Stop warnings + + var err = new Error('Queue cleared'); + + return this.sequencer.clear(err) + .then(function() { + assert.strictEqual(count, 1); + assert(p1.isFulfilled()); + assert.strictEqual(p1.value(), "a"); + + return p2.reflect(); + }) + .then(function(r) { + assert.strictEqual(count, 1); + assert(r.isRejected()); + assert.strictEqual(r.reason(), err); + }); + + }); + + }); + + +}); diff --git a/libs/halley/test/specs/client-advice-spec.js b/libs/halley/test/specs/client-advice-spec.js new file mode 100644 index 0000000..339fa98 --- /dev/null +++ b/libs/halley/test/specs/client-advice-spec.js @@ -0,0 +1,104 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); + +function defer() { + var d = {}; + + d.promise = new Promise(function(resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + + return d; +} + +module.exports = function() { + describe('client-advice', function() { + + it('should handle advice retry', function() { + var publishOccurred = false; + var client = this.client; + + var d = defer(); + + return client.subscribe('/datetime', function() { + if (!publishOccurred) return; + d.resolve(); + }) + .then(function() { + return client.publish('/advice-retry', { data: 1 }); + }) + .then(function() { + publishOccurred = true; + }) + .then(function() { + return d.promise; + }) + }); + + /** + * Tests to ensure that after receiving a handshake advice + */ + it('should handle advice handshake', function() { + var client = this.client; + var originalClientId; + var rehandshook = false; + var d = defer(); + + return client.subscribe('/datetime', function() { + if (!rehandshook) return; + d.resolve(); + }) + .then(function() { + originalClientId = client.getClientId(); + + client.once('connected', function() { + rehandshook = true; + }); + + return client.publish('/advice-handshake', { data: 1 }); + }) + .then(function() { + return d.promise; + }) + .then(function() { + assert(client.getClientId()); + assert.notEqual(client.getClientId(), originalClientId); + }); + }); + + /** + * Ensure the client is disabled after advice:none + */ + it('should handle advice none', function() { + var client = this.client; + var d = defer(); + + client.once('disabled', function() { + d.resolve(); + }); + + client.publish('/advice-none', { data: 1 }); + + return d.promise + .then(function() { + assert(client.stateIs('DISABLED')); + + // Don't reconnect + return client.publish('/advice-none', { data: 1 }) + .then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err.message, 'Client disabled'); + + return client.reset(); + }); + }); + + }); + + }); + +}; diff --git a/libs/halley/test/specs/client-bad-connection-spec.js b/libs/halley/test/specs/client-bad-connection-spec.js new file mode 100644 index 0000000..9f223b0 --- /dev/null +++ b/libs/halley/test/specs/client-bad-connection-spec.js @@ -0,0 +1,129 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); + +function defer() { + var d = {}; + + d.promise = new Promise(function(resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + + return d; +} +var OUTAGE_TIME = 5000; + +module.exports = function() { + describe('client-bad-connection', function() { + + it('should deal with dropped packets', function() { + var count = 0; + var postOutageCount = 0; + var outageTime; + var outageGraceTime; + var self = this; + + var d = defer(); + this.client.subscribe('/datetime', function() { + count++; + + if (count === 1) { + return self.serverControl.networkOutage(OUTAGE_TIME) + .then(function() { + outageTime = Date.now(); + outageGraceTime = Date.now() + 1000; + }) + .catch(function(err) { + d.reject(err); + }); + } + + if (!outageTime) return; + if (outageGraceTime >= Date.now()) return; + + postOutageCount++; + + if (postOutageCount >= 3) { + assert(Date.now() - outageTime >= (OUTAGE_TIME * 0.8)); + d.resolve(); + } + }) + .then(function() { + return d.promise; + }); + }); + + it('should emit connection events', function() { + var client = this.client; + + var d1 = defer(); + var d2 = defer(); + var d3 = defer(); + + client.once('connection:up', function() { + d1.resolve(); + }); + + return client.connect() + .bind(this) + .then(function() { + // Awaiting initial connection:up + return d1.promise; + }) + .then(function() { + client.once('connection:down', function() { + d2.resolve(); + }); + + return client.subscribe('/datetime', function() {}); + }) + .then(function() { + client.once('connection:up', function() { + d3.resolve(); + }); + + return this.serverControl.restart(); + }) + .then(function() { + // connection:down fired + return d2.promise; + }) + .then(function() { + // connection:up fired + return d3.promise; + }); + }); + + it('should emit connection events without messages', function() { + var client = this.client; + + var d1 = defer(); + client.on('connection:down', function() { + d1.resolve(); + }); + + var d2 = defer(); + return client.connect() + .bind(this) + .delay(400) // Give the connection time to send a connect + .then(function() { + client.on('connection:up', function() { + d2.resolve(); + }); + return this.serverControl.restart(); + }) + .then(function() { + // connection:down fired + return d1.promise; + }) + .then(function() { + // connection:up fired + return d2.promise; + }); + }); + + }); + +}; diff --git a/libs/halley/test/specs/client-bad-websockets-spec.js b/libs/halley/test/specs/client-bad-websockets-spec.js new file mode 100644 index 0000000..8ee68ae --- /dev/null +++ b/libs/halley/test/specs/client-bad-websockets-spec.js @@ -0,0 +1,47 @@ +'use strict'; + +var Promise = require('bluebird'); + +function defer() { + var d = {}; + + d.promise = new Promise(function(resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + + return d; +} + +var OUTAGE_TIME = 2000; + +module.exports = function() { + describe('client-bad-websockets', function() { + + it('should deal with bad corporate proxies', function() { + var count = 0; + var self = this; + + var d = defer(); + + return this.serverControl.stopWebsockets(OUTAGE_TIME) + .then(function() { + return self.client.subscribe('/datetime', function() { + count++; + + if (count === 3) { + d.resolve(); + } + }); + }) + .then(function() { + return d.promise; + }); + + + }); + + + }); + +}; diff --git a/libs/halley/test/specs/client-connect-spec.js b/libs/halley/test/specs/client-connect-spec.js new file mode 100644 index 0000000..0682b9a --- /dev/null +++ b/libs/halley/test/specs/client-connect-spec.js @@ -0,0 +1,57 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); + +module.exports = function() { + describe('client-connect', function() { + + it('should not timeout on empty connect messages', function() { + var client = this.client; + var connectionWentDown = false; + client.on('connection:down', function() { + connectionWentDown = true; + }); + + return client.connect() + .then(function() { + return Promise.delay(client._advice.timeout + 1000); + }) + .then(function() { + assert(!connectionWentDown); + }); + }); + + describe('should not try reconnect when denied access', function() { + beforeEach(function() { + this.extension = { + outgoing: function(message, callback) { + if (message.channel === '/meta/handshake') { + message.ext = { deny: true }; + } + callback(message); + } + }; + + this.client.addExtension(this.extension); + }); + + afterEach(function() { + this.client.removeExtension(this.extension); + }); + + it('should disconnect', function() { + var client = this.client; + + return client.connect() + .then(function() { + assert.ok(false); + }, function(e) { + assert.strictEqual(e.message, 'Unauthorised'); + }); + }); + }); + + }); + +}; diff --git a/libs/halley/test/specs/client-delete-spec.js b/libs/halley/test/specs/client-delete-spec.js new file mode 100644 index 0000000..6141d95 --- /dev/null +++ b/libs/halley/test/specs/client-delete-spec.js @@ -0,0 +1,57 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); + +function defer() { + var d = {}; + + d.promise = new Promise(function(resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + + return d; +} + +module.exports = function() { + describe('client-delete', function() { + /** + * This test ensures that the client is able to recover from a situation + * where the server unexpectedly deletes the client and the client + * no longer exists on the server + */ + it('should recover from an unexpected disconnect', function() { + var client = this.client; + var count = 0; + var deleteOccurred = false; + var originalClientId; + var serverControl = this.serverControl; + + var d = defer(); + return client.subscribe('/datetime', function() { + if (!deleteOccurred) return; + count++; + if (count === 3) { + d.resolve(); + } + }).then(function() { + originalClientId = client.getClientId(); + assert(originalClientId); + + return serverControl.deleteSocket(client.getClientId()); + }) + .then(function() { + deleteOccurred = true; + }) + .then(function() { + return d.promise; + }) + .then(function() { + assert.notEqual(originalClientId, client.getClientId()); + }); + }); + + + }); +}; diff --git a/libs/halley/test/specs/client-invalid-endpoint-spec.js b/libs/halley/test/specs/client-invalid-endpoint-spec.js new file mode 100644 index 0000000..4f6c644 --- /dev/null +++ b/libs/halley/test/specs/client-invalid-endpoint-spec.js @@ -0,0 +1,25 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); + +module.exports = function() { + describe('client-invalid-endpoint', function() { + + it('should not connect', function() { + var client = this.client; + var connectionCameUp = false; + client.on('connection:down', function() { + connectionCameUp = true; + }); + + return Promise.any([client.connect(), Promise.delay(5000)]) + .then(function() { + assert(!connectionCameUp); + return client.disconnect(); + }); + }); + + }); + +}; diff --git a/libs/halley/test/specs/client-proxied-spec.js b/libs/halley/test/specs/client-proxied-spec.js new file mode 100644 index 0000000..8963542 --- /dev/null +++ b/libs/halley/test/specs/client-proxied-spec.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = function() { + require('./client-server-restart-spec')(); + require('./client-bad-connection-spec')(); +}; diff --git a/libs/halley/test/specs/client-publish-spec.js b/libs/halley/test/specs/client-publish-spec.js new file mode 100644 index 0000000..0ac1864 --- /dev/null +++ b/libs/halley/test/specs/client-publish-spec.js @@ -0,0 +1,78 @@ +'use strict'; + +var Promise = require('bluebird'); +var assert = require('assert'); + +module.exports = function() { + + describe('client-publish', function() { + + it('should handle publishes', function() { + var client = this.client; + + return client.publish('/channel', { data: 1 }); + }); + + it('should fail when a publish does not work', function() { + return this.client.publish('/devnull', { data: 1 }, { attempts: 1 }) + .then(function() { + throw new Error('Expected failure'); + }, function() { + // Swallow the error + }); + + }); + + it('should handle a large number of publish messages', function() { + var count = 0; + var self = this; + return (function next() { + count++; + if (count >= 10) return; + + return self.client.publish('/channel', { data: count }) + .then(function() { + return next(); + }); + })(); + }); + + it('should handle a parallel publishes', function() { + var count = 0; + var self = this; + return (function next() { + count++; + if (count >= 20) return; + + return Promise.all([ + self.client.publish('/channel', { data: count }), + self.client.publish('/channel', { data: count }), + self.client.publish('/channel', { data: count }), + ]) + .then(function() { + return next(); + }); + })(); + }); + + it('should handle the cancellation of one publish without affecting another', function() { + var p1 = this.client.publish('/channel', { data: 1 }) + .then(function() { + throw new Error('Expected error'); + }); + + var p2 = this.client.publish('/channel', { data: 2 }); + p1.cancel(); + + return p2.then(function(v) { + assert(v.successful); + assert(p1.isCancelled()); + assert(p2.isFulfilled()); + }); + + }); + + + }); + +}; diff --git a/libs/halley/test/specs/client-reset-spec.js b/libs/halley/test/specs/client-reset-spec.js new file mode 100644 index 0000000..b30cc94 --- /dev/null +++ b/libs/halley/test/specs/client-reset-spec.js @@ -0,0 +1,64 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); + +function defer() { + var d = {}; + + d.promise = new Promise(function(resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + + return d; +} + +module.exports = function() { + describe('client-reset', function() { + it('a reset should proceed normally', function() { + var client = this.client; + var originalClientId; + var rehandshook = false; + var count = 0; + var postResetCount = 0; + var d = defer(); + + return client.subscribe('/datetime', function() { + count++; + if (count === 1) { + originalClientId = client.getClientId(); + assert(originalClientId); + client.reset(); + + client.once('connected', function() { + rehandshook = true; + }); + + return; + } + + if (rehandshook) { + postResetCount++; + + // Wait for two messages to arrive after the reset to avoid + // the possiblity of a race condition in which a message + // arrives at the same time as the reset + if (postResetCount > 3) { + d.resolve(); + } + } + }) + .then(function() { + return d.promise; + }) + .then(function() { + assert(client.getClientId()); + assert(client.getClientId() !== originalClientId); + }); + }); + + + }); + +}; diff --git a/libs/halley/test/specs/client-server-restart-spec.js b/libs/halley/test/specs/client-server-restart-spec.js new file mode 100644 index 0000000..f2fdc77 --- /dev/null +++ b/libs/halley/test/specs/client-server-restart-spec.js @@ -0,0 +1,60 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); + +function defer() { + var d = {}; + + d.promise = new Promise(function(resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + + return d; +} + +module.exports = function() { + describe('client-server-restart', function() { + it('should deal with a server restart', function() { + var client = this.client; + var count = 0; + var postOutageCount = 0; + var outageTime; + var clientId; + var d = defer(); + var serverControl = this.serverControl; + + return client.subscribe('/datetime', function() { + count++; + + if (count === 3) { + clientId = client.getClientId(); + + return serverControl.restart() + .then(function() { + outageTime = Date.now(); + }) + .catch(function(err) { + d.reject(err); + }); + } + + if (!outageTime) return; + + postOutageCount++; + + if (postOutageCount >= 3) { + d.resolve(); + } + }).then(function() { + return d.promise; + }) + .then(function() { + // A disconnect should not re-initialise the client + assert.strictEqual(clientId, client.getClientId()); + }); + }); + + }); +}; diff --git a/libs/halley/test/specs/client-spec.js b/libs/halley/test/specs/client-spec.js new file mode 100644 index 0000000..d368fa4 --- /dev/null +++ b/libs/halley/test/specs/client-spec.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function() { + require('./client-connect-spec')(); + require('./client-subscribe-spec')(); + require('./client-publish-spec')(); + require('./client-reset-spec')(); + require('./client-advice-spec')(); + require('./client-delete-spec')(); + require('./client-bad-websockets-spec')(); +}; diff --git a/libs/halley/test/specs/client-subscribe-spec.js b/libs/halley/test/specs/client-subscribe-spec.js new file mode 100644 index 0000000..40c96bc --- /dev/null +++ b/libs/halley/test/specs/client-subscribe-spec.js @@ -0,0 +1,248 @@ +'use strict'; + +var assert = require('assert'); +var Promise = require('bluebird'); +var errors = require('../../lib/util/errors'); + +function defer() { + var d = {}; + + d.promise = new Promise(function(resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + + return d; +} + +module.exports = function() { + describe('client-subscribe', function() { + + it('should subscribe to a channel and receive messages', function() { + var onMessage = defer(); + var onSubscribe = defer(); + + var subscription = this.client.subscribe('/datetime', function() { + onMessage.resolve(); + }, null, function() { + onSubscribe.resolve(); + }); + + return Promise.all([subscription, onMessage.promise, onSubscribe.promise]); + }); + + it('should unsubscribe a subscription correctly', function() { + var count = 0; + var onSubscribeCount = 0; + var d = defer(); + + var subscribe = this.client.subscribe('/datetime', function() { + count++; + if (count === 2) { + d.resolve(); + } + }, null, function() { + onSubscribeCount++; + }); + + return Promise.join(subscribe, d.promise, function(subscription) { + return subscription.unsubscribe(); + }) + .bind(this) + .then(function() { + assert.strictEqual(onSubscribeCount, 1); + assert.deepEqual(this.client.listChannels(), []); + }); + }); + + it('should handle subscriptions that are cancelled before establishment, single', function() { + var onSubscribeCount = 0; + + var subscribe = this.client.subscribe('/datetime', function() { + assert.ok(false); + }, null, function() { + + }); + + return this.client.connect() + .bind(this) + .then(function() { + // The subscribe will be inflight right now + assert(subscribe.isPending()); + return subscribe.unsubscribe(); + }) + .then(function() { + assert.strictEqual(onSubscribeCount, 0); + assert.deepEqual(this.client.listChannels(), []); + }); + }); + + it('should handle subscriptions that are cancelled before establishment, double', function() { + var onSubscribe = 0; + + var subscribe1 = this.client.subscribe('/datetime', function() { + assert.ok(false); + }, null, function() { + onSubscribe++; + }); + + var d = defer(); + + var subscribe2 = this.client.subscribe('/datetime', d.resolve, null, function() { + onSubscribe++; + }); + + return this.client.connect() + .bind(this) + .then(function() { + + // The subscribe will be inflight right now + assert(subscribe1.isPending()); + return subscribe1.unsubscribe(); + }) + .then(function() { + return subscribe2; + }) + .then(function() { + assert.strictEqual(onSubscribe, 1); + assert.deepEqual(this.client.listChannels(), ['/datetime']); + return d.promise; + }); + }); + + it('should handle subscriptions that are cancelled before establishment, then resubscribed', function() { + var subscription = this.client.subscribe('/datetime', function() { }); + + var promise = this.client.connect() + .bind(this) + .then(function() { + return subscription.unsubscribe(); + }) + .then(function() { + var d = defer(); + return Promise.all([this.client.subscribe('/datetime', d.resolve), d.promise]); + }) + .then(function() { + assert.deepEqual(this.client.listChannels(), ['/datetime']); + }); + + // Cancel immediately + subscription.unsubscribe(); + + return promise; + }); + + it('should handle subscription failure correctly', function() { + return this.client.subscribe('/banned', function() { + assert(false); + }) + .then(function() { assert(false); }, function() { }); + }); + + it('should handle subscribe then followed by catch', function() { + return this.client.subscribe('/banned', function() { + assert(false); + }) + .then(function() { + assert(false); + }) + .catch(function(err) { + assert(err instanceof errors.BayeuxError); + assert.strictEqual(err.message, 'Invalid subscription'); + }); + }); + + it('should handle subscribe with catch', function() { + var count = 0; + return this.client.subscribe('/banned', function() { + assert(false); + }) + .catch(function(err) { + assert(err instanceof errors.BayeuxError); + assert.strictEqual(err.message, 'Invalid subscription'); + count++; + }) + .then(function() { + assert.strictEqual(count, 1); + }); + }); + + it('should deal with subscriptions that fail with unknown client', function() { + return this.client.connect() + .bind(this) + .then(function() { + return this.serverControl.deleteSocket(this.client.getClientId()) + .delay(100); // Give the server time to disconnect + }) + .then(function() { + var d = defer(); + + var subscribe = this.client.subscribe('/datetime', d.resolve); + return Promise.all([subscribe, d.promise]); + }); + + }); + + it('cancelling one subscription during handshake should not affect another', function() { + var subscribe1 = this.client.subscribe('/datetime', function() { }); + var subscribe2 = this.client.subscribe('/datetime', function() { }); + + return Promise.delay(1) + .bind(this) + .then(function() { + assert(subscribe1.isPending()); + return [subscribe1.unsubscribe(), subscribe2]; + }) + .spread(function(unsubscribe, subscription2) { + assert.deepEqual(this.client.listChannels(), ['/datetime']); + return subscription2.unsubscribe(); + }) + .then(function() { + assert.deepEqual(this.client.listChannels(), []); + }); + }); + + it('unsubscribing from a channel after a disconnect should not reconnect the client', function() { + return this.client.subscribe('/datetime', function() { }) + .bind(this) + .then(function(subscription) { + return this.client.disconnect() + .bind(this) + .then(function() { + assert(this.client.stateIs('UNCONNECTED')); + return subscription.unsubscribe(); + }) + .then(function() { + assert(this.client.stateIs('UNCONNECTED')); + }); + }); + }); + + describe('extended tests #slow', function() { + + it('should handle multiple subscribe/unsubscribes', function() { + var i = 0; + var client = this.client; + return (function next() { + if (++i > 15) return; + + var subscribe = client.subscribe('/datetime', function() { }); + + return (i % 2 === 0 ? subscribe : Promise.delay(1)) + .then(function() { + return subscribe.unsubscribe(); + }) + .then(function() { + assert.deepEqual(client.listChannels(), []); + }) + .then(next); + })() + .then(function() { + assert.deepEqual(client.listChannels(), []); + }); + }); + }); + + }); + +}; diff --git a/libs/halley/test/specs/websocket-bad-connection-spec.js b/libs/halley/test/specs/websocket-bad-connection-spec.js new file mode 100644 index 0000000..98d5d0f --- /dev/null +++ b/libs/halley/test/specs/websocket-bad-connection-spec.js @@ -0,0 +1,53 @@ +'use strict'; + +var Promise = require('bluebird'); +var globalEvents = require('../../lib/util/global-events'); + +module.exports = function() { + describe('websocket-bad-connection', function() { + + it('should terminate if the server cannot be pinged', function() { + var serverControl = this.serverControl; + + return this.websocket.connect() + .bind(this) + .then(function() { + var self = this; + return Promise.all([ + new Promise(function(resolve) { + self.dispatcher.handleError = function() { + resolve(); + } + }), + serverControl.networkOutage(2000) + ]); + }); + }); + + /** + * This test simulates a network event, such as online/offline detection + * This should make the speed of recovery much faster + */ + it('should terminate if the server cannot be pinged after a network event', function() { + var serverControl = this.serverControl; + + return this.websocket.connect() + .bind(this) + .then(function() { + var self = this; + return Promise.all([ + new Promise(function(resolve) { + self.dispatcher.handleError = function() { + resolve(); + } + }), + serverControl.networkOutage(2000) + .then(function() { + globalEvents.trigger('network'); + }) + ]); + }); + }); + + }); +}; diff --git a/libs/halley/test/specs/websocket-server-restart-spec.js b/libs/halley/test/specs/websocket-server-restart-spec.js new file mode 100644 index 0000000..1cb8dae --- /dev/null +++ b/libs/halley/test/specs/websocket-server-restart-spec.js @@ -0,0 +1,25 @@ +'use strict'; + +var sinon = require('sinon'); + +module.exports = function() { + describe('websocket-server-restart', function() { + + it('should terminate if the server disconnects', function() { + var self = this; + var mock = sinon.mock(this.dispatcher); + mock.expects("handleError").once(); + + return this.websocket.connect() + .bind(this) + .then(function() { + return self.serverControl.restart(); + }) + .delay(10) + .then(function() { + mock.verify(); + }); + }); + + }); +}; diff --git a/libs/halley/test/specs/websocket-spec.js b/libs/halley/test/specs/websocket-spec.js new file mode 100644 index 0000000..9a31377 --- /dev/null +++ b/libs/halley/test/specs/websocket-spec.js @@ -0,0 +1,39 @@ +'use strict'; + +var sinon = require('sinon'); +var Promise = require('bluebird'); +var assert = require('assert'); + +module.exports = function() { + describe('websocket-transport', function() { + + it('should connect', function() { + return this.websocket.connect(); + }); + + it('should cancel connect', function() { + var connect = this.websocket.connect(); + connect.cancel(); + return Promise.delay(100) + .bind(this) + .then(function() { + assert(connect.isCancelled()); + assert(!this.websocket._socket); + assert.strictEqual(this.websocket._connectPromise, null); + }); + }); + + it('should notify on close', function() { + var mock = sinon.mock(this.dispatcher); + mock.expects("handleError").once(); + + return this.websocket.connect() + .bind(this) + .then(function() { + this.websocket.close(); + mock.verify(); + }); + }); + + }); +}; diff --git a/libs/halley/test/statemachine-mixin-test.js b/libs/halley/test/statemachine-mixin-test.js new file mode 100644 index 0000000..d449df3 --- /dev/null +++ b/libs/halley/test/statemachine-mixin-test.js @@ -0,0 +1,559 @@ +'use strict'; + +var StateMachineMixin = require('../lib/mixins/statemachine-mixin'); +var assert = require('assert'); +var extend = require('../lib/util/externals').extend; +var Promise = require('bluebird'); + +describe('statemachine-mixin', function() { + + describe('normal flow', function() { + + beforeEach(function() { + + var TEST_FSM = { + name: "test", + initial: "A", + transitions: { + A: { + t1: "B" + }, + B: { + t2: "C" + }, + C: { + t3: "A" + } + } + }; + + function TestMachine() { + this.initStateMachine(TEST_FSM); + } + + TestMachine.prototype = { + }; + extend(TestMachine.prototype, StateMachineMixin); + + this.testMachine = new TestMachine(); + }); + + it('should transition', function() { + return this.testMachine.transitionState('t1') + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('B')); + }); + }); + + it('should serialize transitions', function() { + return Promise.all([ + this.testMachine.transitionState('t1'), + this.testMachine.transitionState('t2') + ]) + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('C')); + }); + }); + + it('should handle optional transitions', function() { + return this.testMachine.transitionState('doesnotexist', { optional: true }) + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('A')); + }); + }); + + it('should reject invalid transitions', function() { + return this.testMachine.transitionState('doesnotexist') + .bind(this) + .then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err.message, 'Unable to perform transition doesnotexist from state A'); + }); + }); + + it('should proceed with queued transitions after a transition has failed', function() { + return Promise.all([ + this.testMachine.transitionState('doesnotexist'), + this.testMachine.transitionState('t1'), + ].map(function(p) { return p.reflect(); })) + .bind(this) + .spread(function(p1, p2) { + assert(p1.isRejected()); + assert(p2.isFulfilled()); + assert(this.testMachine.stateIs('B')); + }); + + }); + }); + + describe('automatic transitioning', function() { + + beforeEach(function() { + + var TEST_FSM = { + name: "test", + initial: "A", + transitions: { + A: { + t1: "B", + t2: "C" + }, + B: { + t3: "C" + }, + C: { + t4: "A" + } + } + }; + + function TestMachine() { + this.initStateMachine(TEST_FSM); + } + + TestMachine.prototype = { + _onEnterA: function() { + return 't1'; + }, + _onEnterB: function() { + return 't3'; + }, + _onEnterC: function() { + } + }; + extend(TestMachine.prototype, StateMachineMixin); + + this.testMachine = new TestMachine(); + + }); + + it('should transition', function() { + return this.testMachine.transitionState('t1') + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('C')); + }); + }); + + it.skip('should reject on state transitions', function() { + return Promise.all([ + this.testMachine.waitForState({ + rejected: 'B', + fulfilled: 'C' + }), + this.testMachine.transitionState('t1') + ]) + .bind(this) + .then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err.message, 'State is B'); + }); + }); + + it.skip('should wait for state transitions when already in the state', function() { + return this.testMachine.waitForState({ + fulfilled: 'A' + }) + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('A')); + }); + }); + + it.skip('should reject state transitions when already in the state', function() { + return this.testMachine.waitForState({ + fulfilled: 'C', + rejected: 'A' + }) + .bind(this) + .then(function() { + assert.ok(false); + }, function() { + assert.ok(true); + }); + }); + + it.skip('should timeout waiting for state transitions', function() { + return this.testMachine.waitForState({ + fulfilled: 'C', + rejected: 'B', + timeout: 1 + }) + .bind(this) + .then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err.message, 'Timeout waiting for state C'); + }); + }); + + }); + + describe('error handling', function() { + + beforeEach(function() { + + var TEST_FSM = { + name: "test", + initial: "A", + transitions: { + A: { + t1: "B", + t4: "C", + t6: 'FAIL_ON_ENTER' + }, + B: { + t2: "C", + error: 'D' + }, + C: { + t5: 'E' + }, + D: { + t3: "E" + }, + E: { + + }, + FAIL_ON_ENTER: { + + } + } + }; + + function TestMachine() { + this.initStateMachine(TEST_FSM); + } + + TestMachine.prototype = { + _onLeaveC: function() { + }, + _onEnterB: function() { + throw new Error('Failed to enter B'); + }, + _onEnterFAIL_ON_ENTER: function() { + throw new Error('Failed on enter'); + } + }; + extend(TestMachine.prototype, StateMachineMixin); + + this.testMachine = new TestMachine(); + + }); + + it('should handle errors on transition', function() { + return this.testMachine.transitionState('t1') + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('D')); + }); + }); + + it('should transition to error state before other queued transitions', function() { + return Promise.all([ + this.testMachine.transitionState('t1'), + this.testMachine.transitionState('t3'), + ].map(function(p) { return p.reflect(); })) + .bind(this) + .spread(function(p1, p2) { + assert(p1.isFulfilled()); + assert(p2.isFulfilled()); + assert(this.testMachine.stateIs('E')); + }); + }); + + + it('should throw the original error if the state does not have an error transition', function() { + return this.testMachine.transitionState('t6') + .bind(this) + .then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err.message, 'Failed on enter'); + }); + }); + + }); + + describe('dedup', function() { + + beforeEach(function() { + + var TEST_FSM = { + name: "test", + initial: "A", + transitions: { + A: { + t1: "B" + }, + B: { + t1: "C" + }, + C: { + } + } + }; + + function TestMachine() { + this.initStateMachine(TEST_FSM); + } + + TestMachine.prototype = { + _onEnterB: function() { + this.bCount = this.bCount ? this.bCount + 1 : 1; + return Promise.delay(1); + }, + _onEnterC: function() { + return Promise.delay(1); + } + }; + extend(TestMachine.prototype, StateMachineMixin); + + this.testMachine = new TestMachine(); + }); + + it('should transition with dedup', function() { + return Promise.all([ + this.testMachine.transitionState('t1'), + this.testMachine.transitionState('t1', { dedup: true }), + ]) + .bind(this) + .then(function() { + assert.strictEqual(this.testMachine.bCount, 1); + assert(this.testMachine.stateIs('B')); + }); + }); + + it('should clearup pending transitions', function() { + return this.testMachine.transitionState('t1') + .bind(this) + .then(function() { + assert.strictEqual(this.testMachine.bCount, 1); + assert(this.testMachine.stateIs('B')); + assert.deepEqual(this.testMachine._pendingTransitions, {}); + }); + }); + + it('should transition with dedup followed by non-dedup', function() { + return Promise.all([ + this.testMachine.transitionState('t1'), + this.testMachine.transitionState('t1', { dedup: true }), + this.testMachine.transitionState('t1'), + ]) + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('C')); + assert.deepEqual(this.testMachine._pendingTransitions, {}); + }); + }); + + it('should dedup against the first pending transition', function() { + var p1 = this.testMachine.transitionState('t1'); + var p2 = this.testMachine.transitionState('t1'); + var p3 = this.testMachine.transitionState('t1', { dedup: true }) + .bind(this) + .then(function() { + assert(p1.isFulfilled()); + assert(p2.isPending()); + }); + + return Promise.all([p1, p2, p3]) + .bind(this) + .then(function() { + assert(this.testMachine.stateIs('C')); + assert.deepEqual(this.testMachine._pendingTransitions, {}); + }); + }); + + }); + + describe('cancellation', function() { + beforeEach(function() { + + var TEST_FSM = { + name: "test", + initial: "A", + transitions: { + A: { + t1: "B", + }, + B: { + t2: "C", + }, + C: { + t3: "D" + }, + D: { + } + } + }; + + function TestMachine() { + this.count = 0; + this.initStateMachine(TEST_FSM); + } + + TestMachine.prototype = { + _onEnterB: function() { + return Promise.delay(1); + }, + _onEnterC: function() { + return Promise.delay(5) + .bind(this) + .then(function() { + this.count++; + }); + } + }; + extend(TestMachine.prototype, StateMachineMixin); + + this.testMachine = new TestMachine(); + }); + + it('should handle cancellations before they execute', function() { + var p1 = this.testMachine.transitionState('t1'); + var p2 = this.testMachine.transitionState('t2'); + + p2.cancel(); + return Promise.delay(5) + .bind(this) + .then(function() { + assert(!p1.isCancelled()); + return p1; + }) + .then(function() { + assert(this.testMachine.stateIs('B')); + }); + }); + + it('should not cancel transitions after they start', function() { + var p; + return this.testMachine.transitionState('t1') + .bind(this) + .then(function() { + p = this.testMachine.transitionState('t2'); + return Promise.delay(1); + }) + .then(function() { + assert(p.isCancellable()); + p.cancel(); + return Promise.delay(5); + }) + .then(function() { + assert.strictEqual(this.testMachine.count, 1); + assert(this.testMachine.stateIs('C')); + }); + }); + + }); + + + describe('resetTransition', function() { + beforeEach(function() { + var self = this; + var TEST_FSM = { + name: "test", + initial: "A", + globalTransitions: { + disable: "DISABLED" + }, + transitions: { + A: { + t1: "B", + t2: "C", + t5: "D" + }, + B: { + t3: "C" + }, + C: { + t4: "B" + }, + D: { + t6: "A" + }, + DISABLED: { + + } + } + }; + + function TestMachine() { + this.initStateMachine(TEST_FSM); + } + + TestMachine.prototype = { + _onLeaveA: function() { + self.leaveACount++; + }, + _onEnterB: function() { + self.enterBCount++; + return Promise.delay(1, 't3'); + }, + _onLeaveB: function() { + self.leaveBCount++; + }, + _onEnterC: function() { + self.enterCCount++; + return Promise.delay(1, 't4'); + } + }; + extend(TestMachine.prototype, StateMachineMixin); + + this.testMachine = new TestMachine(); + this.leaveACount = 0; + this.enterBCount = 0; + this.leaveBCount = 0; + this.enterCCount = 0; + }); + + it('should transition', function() { + var promise = this.testMachine.transitionState('t1'); + + promise.catch(function() {}); // Prevent warnings here + + var resetReason = new Error('We need to reset'); + return this.testMachine.resetTransition('disable', resetReason) + .bind(this) + .then(function() { + assert.strictEqual(this.leaveACount, 1); + assert.strictEqual(this.enterBCount, 1); + assert.strictEqual(this.leaveBCount, 1); + assert.strictEqual(this.enterCCount, 0); + + assert(this.testMachine.stateIs('DISABLED')); + + return promise; // Ensure the original transition completed + }) + .then(function() { + assert.ok(false); + }, function(err) { + assert.strictEqual(err, resetReason); + }); + }); + + it('should not cancel the first transition', function() { + var promise1 = this.testMachine.transitionState('t5'); + var promise2 = this.testMachine.transitionState('t5'); + var promise3 = this.testMachine.transitionState('tXXX'); + + var resetReason = new Error('We need to reset'); + return this.testMachine.resetTransition('disable', resetReason) + .bind(this) + .then(function() { + assert.strictEqual(this.leaveACount, 1); + assert(this.testMachine.stateIs('DISABLED')); + assert(promise1.isFulfilled()); + assert(promise2.reason(), resetReason); + assert(promise3.reason(), resetReason); + }); + }); + + + }); +}); diff --git a/libs/halley/test/test-suite-browser.js b/libs/halley/test/test-suite-browser.js new file mode 100644 index 0000000..57d1c7e --- /dev/null +++ b/libs/halley/test/test-suite-browser.js @@ -0,0 +1,63 @@ +/* jshint browser:true */ +'use strict'; + +require('../lib/util/externals').use({ + Events: require('backbone-events-standalone'), + extend: require('lodash/object/extend') +}); + +var Promise = require('bluebird'); +Promise.config({ + warnings: true, + longStackTraces: !!window.localStorage.BLUEBIRD_LONG_STACK_TRACES, + cancellation: true +}); + +var RemoteServerControl = require('./helpers/remote-server-control'); + +describe('browser integration tests', function() { + this.timeout(30000); + + before(function() { + this.serverControl = new RemoteServerControl(); + return this.serverControl.setup() + .bind(this) + .then(function(urls) { + this.urls = urls; + }); + }); + + after(function() { + return this.serverControl.teardown(); + }); + + beforeEach(function() { + this.urlDirect = this.urls.bayeux; + this.urlProxied = this.urls.proxied; + this.urlInvalid = 'https://127.0.0.2:65534/bayeux'; + + this.clientOptions = { + retry: 500, + timeout: 1000 + }; + }); + + afterEach(function() { + return this.serverControl.restoreAll(); + }); + + + require('./browser-websocket-test'); + require('./client-long-polling-test'); + require('./client-websockets-test'); + require('./client-all-transports-test'); +}); + +describe('browser unit tests', function() { + require('./errors-test'); + require('./promise-util-test'); + require('./channel-set-test'); + require('./extensions-test'); + require('./transport-pool-test'); + require('./statemachine-mixin-test'); +}); diff --git a/libs/halley/test/test-suite-node.js b/libs/halley/test/test-suite-node.js new file mode 100644 index 0000000..6edab43 --- /dev/null +++ b/libs/halley/test/test-suite-node.js @@ -0,0 +1,78 @@ +'use strict'; + +require('../lib/util/externals').use({ + Events: require('backbone-events-standalone'), + extend: require('lodash/object/extend') +}); + +var Promise = require('bluebird'); +Promise.config({ + warnings: true, + longStackTraces: true, + cancellation: true +}); + +var BayeuxWithProxyServer = require('./helpers/bayeux-with-proxy-server'); + +describe('node-test-suite', function() { + + before(function(done) { + var self = this; + this.server = new BayeuxWithProxyServer('localhost'); + + this.serverControl = this.server; + + this.server.start(function(err, urls) { + if (err) return done(err); + self.urls = urls; + done(); + }); + }); + + after(function(done) { + this.server.stop(done); + }); + + describe('integration tests', function() { + this.timeout(20000); + + before(function() { + /* Give server time to startup */ + this.timeout(20000); + return this.serverControl.restoreAll(); + }); + + beforeEach(function() { + this.urlDirect = this.urls.bayeux; + this.urlProxied = this.urls.proxied; + this.urlInvalid = 'https://127.0.0.2:65534/bayeux'; + + this.clientOptions = { + retry: 500, + timeout: 500 + }; + }); + + afterEach(function() { + return this.serverControl.restoreAll(); + }); + + require('./node-websocket-test'); + require('./client-long-polling-test'); + require('./client-websockets-test'); + require('./client-all-transports-test'); + require('./client-shutdown-test'); + }); + + + describe('unit tests', function() { + require('./errors-test'); + require('./promise-util-test'); + require('./channel-set-test'); + require('./extensions-test'); + require('./transport-pool-test'); + require('./statemachine-mixin-test'); + }); + + +}); diff --git a/libs/halley/test/transport-pool-test.js b/libs/halley/test/transport-pool-test.js new file mode 100644 index 0000000..7e87d5a --- /dev/null +++ b/libs/halley/test/transport-pool-test.js @@ -0,0 +1,242 @@ +'use strict'; + +var TransportPool = require('../lib/transport/pool'); +var assert = require('assert'); +var Promise = require('bluebird'); +var sinon = require('sinon'); + +describe('transport pool', function() { + + beforeEach(function() { + this.PollingTransport = function() { }; + this.PollingTransport.prototype = { + connectionType: 'polling', + close: function() { + + }, + }; + + this.StreamingTransport = function() { }; + + this.StreamingTransport.prototype = { + connect: function() { + return Promise.delay(10); + }, + close: function() { + }, + connectionType: 'streaming' + }; + + this.StreamingFailTransport = function() { }; + this.StreamingFailTransport.prototype = { + connect: function() { + return Promise.delay(2) + .then(function() { + throw new Error('Connect fail'); + }); + }, + close: function() { + }, + connectionType: 'streaming-fail' + }; + + this.PollingTransport.isUsable = + this.StreamingTransport.isUsable = + this.StreamingFailTransport.isUsable = function() { + return true; + }; + + + this.dispatcher = { + handleResponse: function() { + }, + handleError: function() { + } + }; + + this.advice = { + reconnect: 'retry', + interval: 0, + timeout: 1000, + retry: 1 + }; + + this.endpoint = { href: 'http://localhost/bayeux' }; + + this.disabled = []; + + this.registeredTransports = [ + ['streaming-fail', this.StreamingFailTransport], + ['streaming', this.StreamingTransport], + ['polling', this.PollingTransport], + ]; + + this.transportPool = new TransportPool(this.dispatcher, this.endpoint, this.advice, this.disabled, this.registeredTransports); + }); + + describe('get', function() { + it('should return an instance of a transport', function() { + return this.transportPool.get() + .bind(this) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + }); + }); + + it('should return an instance of a polling transport', function() { + this.transportPool.setAllowed(['polling']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + }); + }); + + it('should return an instance of a streaming transport', function() { + this.transportPool.setAllowed(['streaming']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + assert(transport instanceof this.StreamingTransport); + }); + }); + + it('should fail if an async transport is unavailable', function() { + this.transportPool.setAllowed(['streaming-fail']); + return this.transportPool.get() + .bind(this) + .then(function() { + assert.ok(false, 'Expected a failure'); + }, function(e) { + assert.strictEqual(e.message, 'Connect fail'); + }); + }); + + it('should return polling transport and then switch to streaming when it comes online', function() { + this.transportPool.setAllowed(['streaming', 'polling']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + }) + .delay(10) + .then(function() { + return this.transportPool.reevaluate(); + }) + .then(function(transport) { + assert(transport instanceof this.StreamingTransport); + }); + }); + + it('should return polling transport and then switch to streaming when it comes online, even when some transport fail', function() { + this.transportPool.setAllowed(['streaming-fail', 'streaming', 'polling']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + }) + .delay(10) + .then(function() { + return this.transportPool.reevaluate(); + }) + .then(function(transport) { + assert(transport instanceof this.StreamingTransport); + }); + }); + + it('should close transports on close', function() { + this.transportPool.setAllowed(['streaming-fail', 'streaming', 'polling']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + var mock = sinon.mock(transport); + mock.expects("close").once(); + + this.transportPool.close(); + assert.deepEqual(this.transportPool._transports, {}); + mock.verify(); + }); + }); + + it('should reselect on down', function() { + var firstTransport; + + this.transportPool.setAllowed(['streaming', 'streaming-fail']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + var mock = sinon.mock(transport); + mock.expects("close").never(); + + firstTransport = transport; + assert(transport instanceof this.StreamingTransport); + this.transportPool.down(transport); + + mock.verify(); + + return this.transportPool.get(); + }) + .then(function(transport) { + assert(transport instanceof this.StreamingTransport); + assert.notStrictEqual(transport, firstTransport); + }); + }); + + it('should reselect on down with async and sync connections', function() { + this.transportPool.setAllowed(['streaming', 'polling']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + }) + .delay(10) + .then(function() { + return this.transportPool.reevaluate(); + }) + .then(function(transport) { + assert(transport instanceof this.StreamingTransport); + this.transportPool.down(transport); + return this.transportPool.get(); + }) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + }) + .delay(10) + .then(function() { + return this.transportPool.reevaluate(); + }) + .then(function() { + return this.transportPool.get(); + }) + .then(function(transport) { + assert(transport instanceof this.StreamingTransport); + }); + }); + + it('should handle multiple transports going down', function() { + var polling; + this.transportPool.setAllowed(['streaming', 'polling']); + return this.transportPool.get() + .bind(this) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + polling = transport; + }) + .delay(10) + .then(function() { + return this.transportPool.reevaluate(); + }) + .then(function(transport) { + assert(transport instanceof this.StreamingTransport); + this.transportPool.down(transport); + this.transportPool.down(polling); + return this.transportPool.get(); + }) + .then(function(transport) { + assert(transport instanceof this.PollingTransport); + assert.notStrictEqual(transport, polling); + }); + }); + + }); +}); diff --git a/npm-debug.log.1906906186 b/npm-debug.log.1906906186 new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 1021aee..5b3a157 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "android-release": "node node_modules/react-native/local-cli/cli.js run-android --variant=release" }, "dependencies": { - "faye": "^1.2.4", + "halley": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", "lodash": "^4.2.1", "moment": "^2.11.2", "react": "16.0.0-alpha.6", From 1b5bcd0be4936a57edb8a4185c9db0eb678200aa Mon Sep 17 00:00:00 2001 From: Terry Sahaidak Date: Wed, 21 Jun 2017 01:32:01 +0300 Subject: [PATCH 4/5] Fix halley on android --- app/api/faye.js | 11 +- app/modules/realtime.js | 74 --- libs/halley/.ackrc | 4 - libs/halley/.eslintrc.json | 21 - libs/halley/.jshintrc | 4 - libs/halley/.npmignore | 8 - libs/halley/.travis.yml | 9 - libs/halley/README.md | 171 ----- libs/halley/backbone/browser.js | 19 - libs/halley/backbone/node.js | 19 - libs/halley/backbone/package.json | 6 - libs/halley/browser-standalone.js | 19 - libs/halley/gulpfile.js | 183 ----- libs/halley/index.js | 18 - libs/halley/karma.conf.js | 134 ---- libs/halley/lib/main.js | 8 - libs/halley/lib/mixins/statemachine-mixin.js | 191 ------ libs/halley/lib/protocol/advice.js | 192 ------ libs/halley/lib/protocol/channel-set.js | 109 --- libs/halley/lib/protocol/channel.js | 90 --- libs/halley/lib/protocol/client.js | 525 --------------- libs/halley/lib/protocol/dispatcher.js | 298 --------- libs/halley/lib/protocol/envelope.js | 56 -- libs/halley/lib/protocol/extensions.js | 51 -- libs/halley/lib/protocol/scheduler.js | 70 -- .../halley/lib/protocol/subscribe-thenable.js | 44 -- libs/halley/lib/protocol/subscription.js | 33 - .../transport/base-long-polling-transport.js | 59 -- libs/halley/lib/transport/base-websocket.js | 252 ------- libs/halley/lib/transport/browser/.jshintrc | 5 - .../transport/browser/browser-websocket.js | 31 - libs/halley/lib/transport/browser/xhr.js | 134 ---- libs/halley/lib/transport/node/.jshintrc | 5 - libs/halley/lib/transport/node/node-http.js | 109 --- .../lib/transport/node/node-websocket.js | 24 - libs/halley/lib/transport/pool.js | 185 ------ libs/halley/lib/transport/transport.js | 56 -- libs/halley/lib/util/errors.js | 58 -- libs/halley/lib/util/externals.js | 11 - libs/halley/lib/util/global-events.js | 61 -- libs/halley/lib/util/promise-util.js | 303 --------- libs/halley/lib/util/uri.js | 77 --- libs/halley/package.json | 140 ---- libs/halley/test/.eslintrc.json | 7 - libs/halley/test/.jshintrc | 12 - libs/halley/test/browser-websocket-test.js | 51 -- libs/halley/test/channel-set-test.js | 140 ---- .../halley/test/client-all-transports-test.js | 73 -- libs/halley/test/client-long-polling-test.js | 61 -- libs/halley/test/client-shutdown-test.js | 32 - libs/halley/test/client-websockets-test.js | 78 --- libs/halley/test/errors-test.js | 27 - libs/halley/test/extensions-test.js | 39 -- libs/halley/test/helpers/bayeux-server.js | 191 ------ .../test/helpers/bayeux-with-proxy-server.js | 73 -- .../test/helpers/cleanup-test-process.js | 49 -- libs/halley/test/helpers/proxy-server.js | 123 ---- libs/halley/test/helpers/public/index.html | 13 - .../test/helpers/remote-server-control.js | 83 --- libs/halley/test/helpers/server.js | 208 ------ libs/halley/test/node-websocket-test.js | 52 -- libs/halley/test/on-before-unload-test.js | 34 - libs/halley/test/promise-util-test.js | 625 ------------------ libs/halley/test/specs/client-advice-spec.js | 104 --- .../test/specs/client-bad-connection-spec.js | 129 ---- .../test/specs/client-bad-websockets-spec.js | 47 -- libs/halley/test/specs/client-connect-spec.js | 57 -- libs/halley/test/specs/client-delete-spec.js | 57 -- .../specs/client-invalid-endpoint-spec.js | 25 - libs/halley/test/specs/client-proxied-spec.js | 6 - libs/halley/test/specs/client-publish-spec.js | 78 --- libs/halley/test/specs/client-reset-spec.js | 64 -- .../test/specs/client-server-restart-spec.js | 60 -- libs/halley/test/specs/client-spec.js | 11 - .../test/specs/client-subscribe-spec.js | 248 ------- .../specs/websocket-bad-connection-spec.js | 53 -- .../specs/websocket-server-restart-spec.js | 25 - libs/halley/test/specs/websocket-spec.js | 39 -- libs/halley/test/statemachine-mixin-test.js | 559 ---------------- libs/halley/test/test-suite-browser.js | 63 -- libs/halley/test/test-suite-node.js | 78 --- libs/halley/test/transport-pool-test.js | 242 ------- 82 files changed, 7 insertions(+), 7786 deletions(-) delete mode 100644 libs/halley/.ackrc delete mode 100644 libs/halley/.eslintrc.json delete mode 100644 libs/halley/.jshintrc delete mode 100644 libs/halley/.npmignore delete mode 100644 libs/halley/.travis.yml delete mode 100644 libs/halley/README.md delete mode 100644 libs/halley/backbone/browser.js delete mode 100644 libs/halley/backbone/node.js delete mode 100644 libs/halley/backbone/package.json delete mode 100644 libs/halley/browser-standalone.js delete mode 100644 libs/halley/gulpfile.js delete mode 100644 libs/halley/index.js delete mode 100644 libs/halley/karma.conf.js delete mode 100644 libs/halley/lib/main.js delete mode 100644 libs/halley/lib/mixins/statemachine-mixin.js delete mode 100644 libs/halley/lib/protocol/advice.js delete mode 100644 libs/halley/lib/protocol/channel-set.js delete mode 100644 libs/halley/lib/protocol/channel.js delete mode 100644 libs/halley/lib/protocol/client.js delete mode 100644 libs/halley/lib/protocol/dispatcher.js delete mode 100644 libs/halley/lib/protocol/envelope.js delete mode 100644 libs/halley/lib/protocol/extensions.js delete mode 100644 libs/halley/lib/protocol/scheduler.js delete mode 100644 libs/halley/lib/protocol/subscribe-thenable.js delete mode 100644 libs/halley/lib/protocol/subscription.js delete mode 100644 libs/halley/lib/transport/base-long-polling-transport.js delete mode 100644 libs/halley/lib/transport/base-websocket.js delete mode 100644 libs/halley/lib/transport/browser/.jshintrc delete mode 100644 libs/halley/lib/transport/browser/browser-websocket.js delete mode 100644 libs/halley/lib/transport/browser/xhr.js delete mode 100644 libs/halley/lib/transport/node/.jshintrc delete mode 100644 libs/halley/lib/transport/node/node-http.js delete mode 100644 libs/halley/lib/transport/node/node-websocket.js delete mode 100644 libs/halley/lib/transport/pool.js delete mode 100644 libs/halley/lib/transport/transport.js delete mode 100644 libs/halley/lib/util/errors.js delete mode 100644 libs/halley/lib/util/externals.js delete mode 100644 libs/halley/lib/util/global-events.js delete mode 100644 libs/halley/lib/util/promise-util.js delete mode 100644 libs/halley/lib/util/uri.js delete mode 100644 libs/halley/package.json delete mode 100644 libs/halley/test/.eslintrc.json delete mode 100644 libs/halley/test/.jshintrc delete mode 100644 libs/halley/test/browser-websocket-test.js delete mode 100644 libs/halley/test/channel-set-test.js delete mode 100644 libs/halley/test/client-all-transports-test.js delete mode 100644 libs/halley/test/client-long-polling-test.js delete mode 100644 libs/halley/test/client-shutdown-test.js delete mode 100644 libs/halley/test/client-websockets-test.js delete mode 100644 libs/halley/test/errors-test.js delete mode 100644 libs/halley/test/extensions-test.js delete mode 100644 libs/halley/test/helpers/bayeux-server.js delete mode 100644 libs/halley/test/helpers/bayeux-with-proxy-server.js delete mode 100644 libs/halley/test/helpers/cleanup-test-process.js delete mode 100644 libs/halley/test/helpers/proxy-server.js delete mode 100644 libs/halley/test/helpers/public/index.html delete mode 100644 libs/halley/test/helpers/remote-server-control.js delete mode 100644 libs/halley/test/helpers/server.js delete mode 100644 libs/halley/test/node-websocket-test.js delete mode 100644 libs/halley/test/on-before-unload-test.js delete mode 100644 libs/halley/test/promise-util-test.js delete mode 100644 libs/halley/test/specs/client-advice-spec.js delete mode 100644 libs/halley/test/specs/client-bad-connection-spec.js delete mode 100644 libs/halley/test/specs/client-bad-websockets-spec.js delete mode 100644 libs/halley/test/specs/client-connect-spec.js delete mode 100644 libs/halley/test/specs/client-delete-spec.js delete mode 100644 libs/halley/test/specs/client-invalid-endpoint-spec.js delete mode 100644 libs/halley/test/specs/client-proxied-spec.js delete mode 100644 libs/halley/test/specs/client-publish-spec.js delete mode 100644 libs/halley/test/specs/client-reset-spec.js delete mode 100644 libs/halley/test/specs/client-server-restart-spec.js delete mode 100644 libs/halley/test/specs/client-spec.js delete mode 100644 libs/halley/test/specs/client-subscribe-spec.js delete mode 100644 libs/halley/test/specs/websocket-bad-connection-spec.js delete mode 100644 libs/halley/test/specs/websocket-server-restart-spec.js delete mode 100644 libs/halley/test/specs/websocket-spec.js delete mode 100644 libs/halley/test/statemachine-mixin-test.js delete mode 100644 libs/halley/test/test-suite-browser.js delete mode 100644 libs/halley/test/test-suite-node.js delete mode 100644 libs/halley/test/transport-pool-test.js diff --git a/app/api/faye.js b/app/api/faye.js index b789829..54d960a 100644 --- a/app/api/faye.js +++ b/app/api/faye.js @@ -1,4 +1,4 @@ -import Faye from '../../libs/halley/browser-standalone' +import Faye from 'react-native-halley' const noop = () => {} import {NetInfo} from 'react-native' @@ -59,9 +59,12 @@ class SnapshotExt { export default class HalleyClient { constructor({token, snapshotHandler}) { this._client = new Faye.Client('https://ws.gitter.im/bayeux', { - timeout: 60000, - retry: 1000, - interval: 1000 + timeout: 3000, + retry: 3000, + interval: 0, + maxNetworkDelay: 3000, + connectTimeout: 3000, + disconnectTimeout: 1000 }) this._token = token this._snapshotHandler = snapshotHandler diff --git a/app/modules/realtime.js b/app/modules/realtime.js index 4b9de83..574d967 100644 --- a/app/modules/realtime.js +++ b/app/modules/realtime.js @@ -37,7 +37,6 @@ const readByRegxIds = /\/api\/v1\/rooms\/([a-f\d]{24})\/chatMessages\/([a-f\d]{2 */ export function setupFaye() { -<<<<<<< HEAD return (dispatch, getState) => { console.log('CONNECT TO FAYE') client = new HalleyClient({ @@ -46,35 +45,6 @@ export function setupFaye() { }) client.setup() client.create() -======= - return async (dispatch, getState) => { - console.log('RECONNECT TO FAYE') - FayeGitter.setAccessToken(getState().auth.token) - FayeGitter.create() - FayeGitter.logger() - try { - dispatch({type: FAYE_CONNECTING}) - const result = await FayeGitter.connect() - dispatch({type: FAYE_CONNECT, payload: result}) - // dispatch(subscribeToChannels()) - } catch (err) { - console.log(err) // eslint-disable-line no-console - } - } -} - -export function checkFayeConnection() { - return async (dispatch, getState) => { - try { - const connectionStatus = await FayeGitter.checkConnectionStatus() - // console.log('CONNECTION_STATUS', connectionStatus) - if (!connectionStatus) { - await dispatch(setupFaye()) - } - } catch (error) { - console.error(error.message) - } ->>>>>>> acf95f0673ac3b8a23f7b181d3d4f6c57032a564 } } @@ -95,12 +65,7 @@ export function onNetStatusChangeFaye(status) { function dispatchMessageEvent(message) { return (dispatch, getState) => { -<<<<<<< HEAD console.log('MESSAGE', message) -======= - // console.log('MESSAGE', event) - const message = JSON.parse(event.json) ->>>>>>> acf95f0673ac3b8a23f7b181d3d4f6c57032a564 const {id} = getState().viewer.user const {activeRoom} = getState().rooms @@ -279,7 +244,6 @@ export function subscribeToReadBy(roomId, messageId) { export function unsubscribeFromReadBy(roomId, messageId) { return async (dispatch) => { -<<<<<<< HEAD try { const url = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` const type = 'roomEvents' @@ -290,44 +254,6 @@ export function unsubscribeFromReadBy(roomId, messageId) { dispatch({type: UNSUBSCRIBE_FROM_READ_BY, roomId}) } catch (err) { console.log(err) -======= - await checkFayeConnection() - const subscription = `/api/v1/rooms/${roomId}/chatMessages/${messageId}/readBy` - FayeGitter.unsubscribe(subscription) - dispatch({type: UNSUBSCRIBE_FROM_READ_BY, roomId}) - dispatch(deleteSubscription(subscription)) - } -} - -export function pushSubscription(subscription) { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (!subscriptions.find(item => item === subscription)) { - dispatch({type: PUSH_SUBSCRIPTION, subscription}) - // console.log('PUSH_SUBSCRIPTION', subscription) - } - } -} - -export function deleteSubscription(subscription) { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (!!subscriptions.find(item => item === subscription)) { - dispatch({type: DELETE_SUBSCRIPTION, subscription}) - // console.log('DELETE_SUBSCRIPTION', subscription) - } - } -} - -export function subscribeToChannels() { - return (dispatch, getState) => { - const {subscriptions} = getState().realtime - if (subscriptions.length === 0) { - dispatch(subscribeToRooms()) - } else { - subscriptions.forEach(subscription => FayeGitter.subscribe(subscription)) - dispatch({type: SUBSCRIBED_TO_CHANNELS, subscriptions}) ->>>>>>> acf95f0673ac3b8a23f7b181d3d4f6c57032a564 } } } diff --git a/libs/halley/.ackrc b/libs/halley/.ackrc deleted file mode 100644 index 2edb143..0000000 --- a/libs/halley/.ackrc +++ /dev/null @@ -1,4 +0,0 @@ ---ignore-dir -dist/ ---ignore-dir -node_modules/ diff --git a/libs/halley/.eslintrc.json b/libs/halley/.eslintrc.json deleted file mode 100644 index 3ca8209..0000000 --- a/libs/halley/.eslintrc.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "rules": { - "quotes": [ - 2, - "single" - ], - "linebreak-style": [ - 2, - "unix" - ], - "semi": [ - 2, - "always" - ] - }, - "env": { - "node": true, - "browser": true - }, - "extends": "eslint:recommended" -} diff --git a/libs/halley/.jshintrc b/libs/halley/.jshintrc deleted file mode 100644 index 0441126..0000000 --- a/libs/halley/.jshintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "node": true, - "unused": true -} diff --git a/libs/halley/.npmignore b/libs/halley/.npmignore deleted file mode 100644 index 9ad5fd1..0000000 --- a/libs/halley/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -*.gem -build -Gemfile.lock -node_modules -.wake.json -coverage -dist -.env diff --git a/libs/halley/.travis.yml b/libs/halley/.travis.yml deleted file mode 100644 index ed95b51..0000000 --- a/libs/halley/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: node_js -node_js: -- '0.12' -- '0.10' -- '4.2' -before_script: - - npm install -g gulp -script: gulp -after_success: ./node_modules/.bin/coveralls --verbose < dist/coverage/lcov.info diff --git a/libs/halley/README.md b/libs/halley/README.md deleted file mode 100644 index 7ed6067..0000000 --- a/libs/halley/README.md +++ /dev/null @@ -1,171 +0,0 @@ -# Halley - -[![Join the chat at https://gitter.im/gitterHQ/halley](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gitterHQ/halley?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/gitterHQ/halley.svg)](https://travis-ci.org/gitterHQ/halley) [![Coverage Status](https://coveralls.io/repos/gitterHQ/halley/badge.svg?branch=master&service=github)](https://coveralls.io/github/gitterHQ/halley?branch=master) - -Halley is an experimental fork of James Coglan's excellent Faye library. - -## Differences from Faye - -The main differences from Faye are (listed in no particular order): -* **Uses promises** (and Bluebird's promise cancellation feature) to do the heavy-lifting whereever possible. -* No Ruby client or server and no server support. Halley is a **Javascript Bayeux client only** -* **Webpack/browserify packaging** -* **Client reset support**. This will force the client to rehandshake. This can be useful when the application realises that the connection is dead before the bayeux client does and allows for faster recovery in these situations. -* **No eventsource support** as we've found them to be unreliable in a ELB/haproxy setup -* All **durations are in milliseconds**, not seconds -* Wherever possible, implementations have been replaced with external libraries: - * Uses [bluebird](https://github.com/petkaantonov/bluebird/) for promises - * Uses backbone events (or backbone-events-standalone) for events - * Mocha/sinon/karma for testing - -## Why's it called "Halley"? - -Lots of reasons! Halley implements the Bayeux Protocol. The [Bayeux Tapestry](https://en.wikipedia.org/wiki/Bayeux_Tapestry) -contains the first know depiction of Halley's Comet. Halley is a [cometd](https://cometd.org) client. - -### Usage - -### Basic Example - -```js -var Halley = require('halley'); -var client = new Halley.Client('/bayeux'); - -function onMessage(message) { - console.log('Incoming message', message); -} - -client.subscribe('/channel', onMessage); - -client.publish('/channel2', { value: 1 }) - .then(function(response) { - console.log('Publish returned', response); - }) - .catch(function(err) { - console.error('Publish failed:', err); - }); -``` - -### Advanced Example - -```js -var Halley = require('halley'); -var Promise = require('bluebird'); - -/** Create a client (showing the default options) */ -var client = new Halley.Client('/bayeux', { - /* The amount of time to wait (in ms) between successive - * retries on a single message send */ - retry: 30000, - - /** - * An integer representing the minimum period of time, in milliseconds, for a - * client to delay subsequent requests to the /meta/connect channel. - * A negative period indicates that the message should not be retried. - * A client MUST implement interval support, but a client MAY exceed the - * interval provided by the server. A client SHOULD implement a backoff - * strategy to increase the interval if requests to the server fail without - * new advice being received from the server. - */ - interval: 0, - - /** - * An integer representing the period of time, in milliseconds, for the - * server to delay responses to the /meta/connect channel. - * This value is merely informative for clients. Bayeux servers SHOULD honor - * timeout advices sent by clients. - */ - timeout: 30000, - - /** - * The maximum number of milliseconds to wait before considering a - * request to the Bayeux server failed. - */ - maxNetworkDelay: 30000, - - /** - * The maximum number of milliseconds to wait for a WebSocket connection to - * be opened. It does not apply to HTTP connections. - */ - connectTimeout: 30000, - - /** - * Maximum time to wait on disconnect - */ - disconnectTimeout: 10000 -}); - -function onMessage(message) { - console.log('Incoming message', message); -} - -/* - *`.subscribe` returns a thenable with a `.unsubscribe` method - * but will also resolve as a promise - */ -var subscription = client.subscribe('/channel', onMessage); - -subscription - .then(function() { - console.log('Subscription successful'); - }) - .catch(function(err) { - console.log('Subscription failed: ', err); - }); - -/** As an example, wait 10 seconds and cancel the subscription */ -Promise.delay(10000) - .then(function() { - return subscription.unsubscribe(); - }); -``` - - -### Debugging - -Halley uses [debug](https://github.com/visionmedia/debug) for debugging. - - * To enable in nodejs, `export DEBUG=halley:*` - * To enable in a browser, `window.localStorage.debug='halley:*'` - -To limit the amount of debug logging produced, you can specify individual categories, eg `export DEBUG=halley:client`. - -## Tests - -Most of the tests in Halley are end-to-end integration tests, which means running a server environment alongside client tests which run in the browser. - -In order to isolate tests from one another, the server will spawn a new Faye server and Proxy server for each test (and tear them down when the test is complete). - -Some of the tests connect to Faye directly, while other tests are performed via the Proxy server which is intended to simulate an reverse-proxy/ELB situation common in many production environments. - -The tests do horrible things in order to test some of the situations we've discovered when using Bayeux and websockets on the web. Examples of things we test to ensure that the client recovers include: - -* Corrupting websocket streams, like bad MITM proxies sometimes do -* Dropping random packets -* Restarting the server during the test -* Deleting the client connection from the server during the test -* Not communicating TCP disconnects from the server-to-client and client-to-server when communicating via the proxy (a situation we've seen on ELB) - -## License - -(The MIT License) - -Copyright (c) 2009-2014 James Coglan and contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the 'Software'), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/libs/halley/backbone/browser.js b/libs/halley/backbone/browser.js deleted file mode 100644 index 18a9bee..0000000 --- a/libs/halley/backbone/browser.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); -Promise.config({ - cancellation: true -}); - -require('../lib/util/externals').use({ - Events: require('backbone').Events, - extend: require('underscore').extend -}); - -var Transport = require('../lib/transport/transport'); - -/* Register the transports. Order is important */ -Transport.register('websocket' , require('../lib/transport/browser/browser-websocket')); -Transport.register('long-polling' , require('../lib/transport/browser/xhr')); - -module.exports = require('../lib/main'); diff --git a/libs/halley/backbone/node.js b/libs/halley/backbone/node.js deleted file mode 100644 index 4ad7256..0000000 --- a/libs/halley/backbone/node.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); -Promise.config({ - cancellation: true -}); - -require('../lib/util/externals').use({ - Events: require('backbone').Events, - extend: require('underscore').extend -}); - -var Transport = require('../lib/transport/transport'); - -/* Register the transports. Order is important */ -Transport.register('websocket' , require('../lib/transport/node/node-websocket')); -Transport.register('long-polling', require('../lib/transport/node/node-http')); - -module.exports = require('../lib/main'); diff --git a/libs/halley/backbone/package.json b/libs/halley/backbone/package.json deleted file mode 100644 index daee103..0000000 --- a/libs/halley/backbone/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "halley-backbone", - "version": "1.0.0", - "main": "./node.js", - "browser": "./browser.js" -} diff --git a/libs/halley/browser-standalone.js b/libs/halley/browser-standalone.js deleted file mode 100644 index fc1eadf..0000000 --- a/libs/halley/browser-standalone.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); -Promise.config({ - cancellation: true -}); - -require('./lib/util/externals').use({ - Events: require('backbone-events-standalone'), - extend: require('lodash/object/extend') -}); - -var Transport = require('./lib/transport/transport'); - -/* Register the transports. Order is important */ -Transport.register('websocket' , require('./lib/transport/browser/browser-websocket')); -Transport.register('long-polling' , require('./lib/transport/browser/xhr')); - -module.exports = require('./lib/main'); diff --git a/libs/halley/gulpfile.js b/libs/halley/gulpfile.js deleted file mode 100644 index 209ac2c..0000000 --- a/libs/halley/gulpfile.js +++ /dev/null @@ -1,183 +0,0 @@ -/* jshint node:true, unused:true */ -'use strict'; - -var gulp = require('gulp'); -var webpack = require('gulp-webpack'); -var gzip = require('gulp-gzip'); -var sourcemaps = require('gulp-sourcemaps'); -var gutil = require('gulp-util'); -var webpack = require('webpack'); -var uglify = require('gulp-uglify'); -var mocha = require('gulp-spawn-mocha'); -var KarmaServer = require('karma').Server; - -gulp.task("webpack-standalone", function(callback) { - // run webpack - webpack({ - entry: "./browser-standalone.js", - output: { - path: "dist/", - filename: "halley.js", - libraryTarget: "umd", - library: "Halley" - }, - stats: true, - failOnError: true, - node: { - console: false, - global: true, - process: false, - Buffer: false, - __filename: false, - __dirname: false, - setImmediate: false - }, - }, function(err, stats) { - if(err) throw new gutil.PluginError("webpack", err); - gutil.log("[webpack]", stats.toString({ - // output options - })); - callback(); - }); -}); - -gulp.task("webpack-backbone", function(callback) { - // run webpack - webpack({ - entry: "./backbone.js", - output: { - path: "dist/", - filename: "halley-backbone.js", - libraryTarget: "umd", - library: "Halley" - }, - externals: { - "backbone": "Backbone", - "underscore": "_" - }, - stats: true, - failOnError: true, - node: { - console: false, - global: true, - process: false, - Buffer: false, - __filename: false, - __dirname: false, - setImmediate: false - }, - }, function(err, stats) { - if(err) throw new gutil.PluginError("webpack", err); - gutil.log("[webpack]", stats.toString({ - // output options - })); - callback(); - }); -}); -gulp.task('webpack', ['webpack-backbone', 'webpack-standalone']); - -gulp.task('uglify', ['webpack'], function() { - return gulp.src('dist/*.js', { base: 'dist/' }) - .pipe(sourcemaps.init({ loadMaps: true })) - .pipe(uglify({ - - })) - .pipe(sourcemaps.write(".")) - .pipe(gulp.dest('dist/min')); -}); - -gulp.task('gzip', ['uglify'], function () { - return gulp.src(['dist/min/**'], { stat: true }) - .pipe(gzip({ append: true, gzipOptions: { level: 9 } })) - .pipe(gulp.dest('dist/min')); -}); - -gulp.task("webpack-test-suite-browser", function(callback) { - // run webpack - webpack({ - entry: "./test/integration/public/test-suite-browser.js", - output: { - path: "dist/", - filename: "test-suite-browser.js", - }, - stats: true, - resolve: { - alias: { - sinon: 'sinon-browser-only' - } - }, - module: { - noParse: [ - /sinon-browser-only/ - ] - }, - devtool: "#eval", - failOnError: true, - node: { - console: false, - global: true, - process: true, - Buffer: false, - __filename: false, - __dirname: false, - setImmediate: false - }, - }, function(err, stats) { - if(err) throw new gutil.PluginError("webpack", err); - gutil.log("[webpack]", stats.toString({ - // output options - })); - callback(); - }); -}); - -gulp.task('test-coverage', function() { - return gulp.src(['test/test-suite-node.js'], { read: false }) - .pipe(mocha({ - exposeGc: true, - istanbul: { - dir: 'dist/coverage' - } - })); -}); - -gulp.task('test', function() { - return gulp.src(['test/test-suite-node.js'], { read: false }) - .pipe(mocha({ - exposeGc: true - })); -}); - -gulp.task('karma', function (done) { - var fork = require('child_process').fork; - - var child = fork('./test/helpers/server'); - - process.on('exit', function() { - child.kill(); - }); - - setTimeout(function() { - function karmaComplete(err) { - if (err) { - console.log('ERROR IS') - console.log(err.stack); - } - child.kill(); - done(err); - } - - var karma = new KarmaServer({ - configFile: __dirname + '/karma.conf.js', - browsers: ['Firefox', 'Chrome', 'Safari'], - singleRun: true, - concurrency: 1 - }, karmaComplete); - - karma.start(); - - }, 1000); - -}); - -gulp.task('default', ['webpack', 'uglify', 'gzip', 'test-coverage']); diff --git a/libs/halley/index.js b/libs/halley/index.js deleted file mode 100644 index 4caf251..0000000 --- a/libs/halley/index.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); -Promise.config({ - cancellation: true -}); - -require('./lib/util/externals').use({ - Events: require('backbone-events-standalone'), - extend: require('lodash/object/extend') -}); - -var Transport = require('./lib/transport/transport'); - -Transport.register('websocket' , require('./lib/transport/node/node-websocket')); -Transport.register('long-polling', require('./lib/transport/node/node-http')); - -module.exports = require('./lib/main'); diff --git a/libs/halley/karma.conf.js b/libs/halley/karma.conf.js deleted file mode 100644 index 2e01791..0000000 --- a/libs/halley/karma.conf.js +++ /dev/null @@ -1,134 +0,0 @@ -var webpack = require('webpack'); -var internalIp = require('internal-ip'); - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'], - - - // list of files / patterns to load in the browser - files: [ - 'test/test-suite-browser.js' - ], - - - // list of files to exclude - exclude: [ - ], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'test/test-suite-browser.js': ['webpack'] - }, - - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress'], - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: false, - - browserStack: { - username: process.env.BROWSERSTACK_USERNAME, - accessKey: process.env.BROWSERSTACK_KEY - }, - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome', 'Firefox', 'Safari'], -/* - customLaunchers: { - bs_firefox_mac: { - base: 'BrowserStack', - browser: 'firefox', - browser_version: '21.0', - os: 'OS X', - os_version: 'Mountain Lion' - }, - bs_iphone5: { - base: 'BrowserStack', - device: 'iPhone 5', - os: 'ios', - os_version: '6.0' - } - }, - - browsers: ['bs_firefox_mac'], -*/ - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: true, - - webpack: { - resolve: { - alias: { - sinon: 'sinon-browser-only' - } - }, - module: { - noParse: [ - /sinon-browser-only/ - ] - }, - devtool: 'inline-source-map', - node: { - console: false, - global: true, - process: true, - Buffer: false, - __filename: false, - __dirname: false, - setImmediate: false - }, - plugins: [ - new webpack.DefinePlugin({ - HALLEY_TEST_SERVER: JSON.stringify('http://' + internalIp.v4() + ':8000') - }) - ] - }, - - webpackMiddleware: { - - }, - - plugins: [ - require("karma-mocha"), - require("karma-chrome-launcher"), - require("karma-firefox-launcher"), - require("karma-safari-launcher"), - require("karma-browserstack-launcher"), - require("karma-webpack") - ], - - captureTimeout: 60000, - // to avoid DISCONNECTED messages - browserDisconnectTimeout : 10000, // default 2000 - browserDisconnectTolerance : 1, // default 0 - browserNoActivityTimeout : 60000, //default 10000 - }); -}; diff --git a/libs/halley/lib/main.js b/libs/halley/lib/main.js deleted file mode 100644 index 9241e05..0000000 --- a/libs/halley/lib/main.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -module.exports = { - Client: require('./protocol/client'), - VERSION: '2.0.0', - BAYEUX_VERSION: '1.0', - Promise: require('bluebird') -}; diff --git a/libs/halley/lib/mixins/statemachine-mixin.js b/libs/halley/lib/mixins/statemachine-mixin.js deleted file mode 100644 index 8e4d1c9..0000000 --- a/libs/halley/lib/mixins/statemachine-mixin.js +++ /dev/null @@ -1,191 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); -var Sequencer = require('../util/promise-util').Sequencer; -var cancelBarrier = require('../util/promise-util').cancelBarrier; -var debug = require('debug')('halley:fsm'); - -var StateMachineMixin = { - initStateMachine: function(config) { - this._config = config; - this._state = config.initial; - this._sequencer = new Sequencer(); - this._pendingTransitions = {}; - this._stateReset = false; - }, - - getState: function() { - return this._state; - }, - - stateIs: function() { - // 99% case optimisation - if (arguments.length === 1) { - return this._state === arguments[0]; - } - - for(var i = 0; i < arguments.length; i++) { - if(this._state === arguments[i]) return true; - } - - return false; - }, - - transitionState: function(transition, options) { - // No new states can be queued during a reset - if (this._stateReset) return Promise.reject(this._stateReset); - - var pending = this._pendingTransitions; - - if (options && options.dedup) { - // The caller can specify that it there is already - // a pending transition of the given type - // wait on that, rather than queueing another. - var pendingTransition = this._findPending(transition); - - if (pendingTransition) { - debug('transition state: %s (dedup)', transition); - return pendingTransition; - } - } - - var next = this._sequencer.chain(function() { - return this._dequeueTransition(transition, options); - }.bind(this)); - - if (!pending[transition]) { - // If this is the first transition of it's type - // save it for deduplication - pending[transition] = next; - } - - return next.finally(function() { - if (pending[transition] === next) { - delete pending[transition]; - } - }); - }, - - resetTransition: Promise.method(function(transition, reason, options) { - if (this._stateReset) return; // TODO: consider - - if (options && options.dedup) { - var pending = this._findPending(transition); - if (pending) { - debug('transition state: %s (dedup)', transition); - return pending; - } - } - - this._stateReset = reason; - - return this._sequencer.clear(reason) - .bind(this) - .then(function() { - this._stateReset = null; - return this.transitionState(transition, options); - }); - }), - - _findPending: function(transition) { - var pending = this._pendingTransitions; - - // The caller can specify that it there is already - // a pending transition of the given type - // wait on that, rather than queueing another. - return pending[transition]; - }, - - _dequeueTransition: Promise.method(function(transition, options) { - var optional = options && options.optional; - - debug('%s: Performing transition: %s', this._config.name, transition); - var newState = this._findTransition(transition); - if (!newState) { - if(!optional) { - throw new Error('Unable to perform transition ' + transition + ' from state ' + this._state); - } - - return null; - } - - if (newState === this._state) return null; - - debug('%s: leave: %s', this._config.name, this._state); - this._triggerStateLeave(this._state, newState); - - var oldState = this._state; - this._state = newState; - - debug('%s enter:%s', this._config.name, this._state); - var promise = this._triggerStateEnter(this._state, oldState) - .bind(this) - .catch(this._transitionError) - .then(function(nextTransition) { - if (nextTransition) { - if (this._stateReset) { - // No automatic transition on state reset - throw this._stateReset; - } - - return this._dequeueTransition(nextTransition); - } - - return null; - }) - - - // State transitions can't be cancelled - return cancelBarrier(promise); - }), - - /* Find the next state, given the current state and a transition */ - _findTransition: function(transition) { - var currentState = this._state; - var transitions = this._config.transitions; - var newState = transitions[currentState] && transitions[currentState][transition]; - if (newState) return newState; - - var globalTransitions = this._config.globalTransitions; - return globalTransitions && globalTransitions[transition]; - }, - - _triggerStateLeave: function(currentState, nextState) { - var handler = this['_onLeave' + currentState]; - if (handler) { - return handler.call(this, nextState); - } - }, - - _triggerStateEnter: Promise.method(function(newState, oldState) { - if (this.onStateChange) { - this.onStateChange(newState, oldState); - } - - var handler = this['_onEnter' + newState]; - if (handler) { - return handler.call(this, oldState) || null; - } - - return null; - }), - - _transitionError: function(err) { - // No automatic transitions on stateReset - if (this._stateReset) throw err; - - var state = this._state; - debug('Error while entering state %s: %s', state, err); - - // Check if the state has a error transition - var errorTransitionState = this._findTransition('error'); - if (errorTransitionState) { - return this._dequeueTransition('error'); - } - - /* No error handler, just throw */ - throw err; - } -}; - -module.exports = StateMachineMixin; diff --git a/libs/halley/lib/protocol/advice.js b/libs/halley/lib/protocol/advice.js deleted file mode 100644 index 7e93566..0000000 --- a/libs/halley/lib/protocol/advice.js +++ /dev/null @@ -1,192 +0,0 @@ -'use strict'; - -var Events = require('../util/externals').Events; -var extend = require('../util/externals').extend; -var debug = require('debug')('halley:advice'); - -var DEFAULT_MAX_NETWORK_DELAY = 30000; -var DEFAULT_ESTABLISH_TIMEOUT = 30000; -var DEFAULT_INTERVAL = 0; -var DEFAULT_TIMEOUT = 30000; -var DEFAULT_DISCONNECT_TIMEOUT = 10000; -var DEFAULT_RETRY_INTERVAL = 1000; -var DEFAULT_CONNECT_TIMEOUT_PADDING = 500; - -var ADVICE_HANDSHAKE = 'handshake'; -var ADVICE_RETRY = 'retry'; -var ADVICE_NONE = 'none'; - -/** - * After three successive failures, make sure we're not trying to quickly - */ -var HANDSHAKE_FAILURE_THRESHOLD = 3; -var HANDSHAKE_FAILURE_MIN_INTERVAL = 1000; - -var MAX_PING_INTERVAL = 50000; -var MIN_PING_INTERVAL = 1000; - -var MIN_ESTABLISH_TIMEOUT = 500; -var MAX_ESTABLISH_TIMEOUT = 60000; - -function Advice(options) { - /* Server controlled values */ - - var apply = function (key, defaultValue) { - if (options[key] !== undefined) { - this[key] = options[key]; - } else { - this[key] = defaultValue; - } - }.bind(this); - - /** - * An integer representing the minimum period of time, in milliseconds, for a - * client to delay subsequent requests to the /meta/connect channel. - * A negative period indicates that the message should not be retried. - * A client MUST implement interval support, but a client MAY exceed the - * interval provided by the server. A client SHOULD implement a backoff - * strategy to increase the interval if requests to the server fail without - * new advice being received from the server. - */ - apply('interval', DEFAULT_INTERVAL); - - /** - * An integer representing the period of time, in milliseconds, for the - * server to delay responses to the /meta/connect channel. - * This value is merely informative for clients. Bayeux servers SHOULD honor - * timeout advices sent by clients. - */ - apply('timeout', DEFAULT_TIMEOUT); - - /* Client values */ - - /** - * The amount of time to wait between successive retries on a - * single message send - */ - apply('retry', DEFAULT_RETRY_INTERVAL); - - - /** - * The maximum number of milliseconds to wait before considering a - * request to the Bayeux server failed. - */ - apply('maxNetworkDelay', DEFAULT_MAX_NETWORK_DELAY); - - /** - * The maximum number of milliseconds to wait for an HTTP connection to - * be opened, and return headers (but not body). In the case of a - * websocket, it's the amount of time to wait for an upgrade - */ - apply('establishTimeout', DEFAULT_ESTABLISH_TIMEOUT); - - /** - * Maximum time to wait on disconnect - */ - apply('disconnectTimeout', DEFAULT_DISCONNECT_TIMEOUT); - - /** - * Additional time to allocate to a connect message - * to avoid it timing out due to network latency. - */ - apply('connectTimeoutPadding', DEFAULT_CONNECT_TIMEOUT_PADDING); - - - - this._handshakes = 0; -} - -Advice.prototype = { - update: function(newAdvice) { - var advice = this; - - var adviceUpdated = false; - ['timeout', 'interval'].forEach(function(key) { - if (newAdvice[key] && newAdvice[key] !== advice[key]) { - adviceUpdated = true; - advice[key] = newAdvice[key]; - } - }); - - if (adviceUpdated) { - debug('Advice updated to interval=%s, timeout=%s using %j', this.interval, this.timeout, newAdvice); - } - - switch(newAdvice.reconnect) { - case ADVICE_HANDSHAKE: - this.trigger('advice:handshake'); - break; - - case ADVICE_NONE: - this.trigger('advice:none'); - break; - } - - }, - - handshakeFailed: function() { - this._handshakes++; - }, - - handshakeSuccess: function() { - this._handshakes = 0; - }, - - getMaxNetworkDelay: function() { - return Math.min(this.timeout, this.maxNetworkDelay); - }, - - getHandshakeInterval: function() { - var interval = this.interval; - - if (this._handshakes > HANDSHAKE_FAILURE_THRESHOLD && interval < HANDSHAKE_FAILURE_MIN_INTERVAL) { - return HANDSHAKE_FAILURE_MIN_INTERVAL; - } - - return interval; - }, - - getDisconnectTimeout: function() { - return Math.min(this.timeout, this.disconnectTimeout); - }, - - getPingInterval: function() { - // If the interval exceeds a minute theres a good chance an ELB or - // intermediate proxy will shut the connection down, so we set - // the interval to 50 seconds max - var pingInterval = this.timeout / 2; - return withinRange(pingInterval, MIN_PING_INTERVAL, MAX_PING_INTERVAL); - }, - - // This represents the amount of time allocated for a - // ``/meta/connect` message request-response cycle - // taking into account network latency - getConnectResponseTimeout: function() { - var padding = Math.min(this.connectTimeoutPadding, this.timeout / 4); - return this.timeout + padding; - }, - - getEstablishTimeout: function() { - // Upper-bound is either a minute, or three-quarters of the timeout value, - // whichever is lower - return withinRange(this.establishTimeout, - MIN_ESTABLISH_TIMEOUT, - Math.min(MAX_ESTABLISH_TIMEOUT, this.timeout * 0.75)); - } - -}; - -extend(Advice.prototype, Events); - - -/** - * Ensure `value` is within [min, max] - */ -function withinRange(value, min, max) { - if (isNaN(value)) return min; - if (value < min) return min; - if (value > max) return max; - return value; -} - -module.exports = Advice; diff --git a/libs/halley/lib/protocol/channel-set.js b/libs/halley/lib/protocol/channel-set.js deleted file mode 100644 index 0663749..0000000 --- a/libs/halley/lib/protocol/channel-set.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; - -var Channel = require('./channel'); -var Promise = require('bluebird'); -var Synchronized = require('../util/promise-util').Synchronized; -var debug = require('debug')('halley:channel-set'); - -function ChannelSet(onSubscribe, onUnsubscribe) { - this._onSubscribe = onSubscribe; - this._onUnsubscribe = onUnsubscribe; - this._channels = {}; - this._syncronized = new Synchronized(); -} - -ChannelSet.prototype = { - get: function(name) { - return this._channels[name]; - }, - - getKeys: function() { - return Object.keys(this._channels); - }, - - /** - * Returns a promise of a subscription - */ - subscribe: function(channelName, subscription) { - // All subscribes and unsubscribes are synchonized by - // the channel name to prevent inconsistent state - return this._sync(channelName, function() { - return this._syncSubscribe(channelName, subscription); - }); - }, - - unsubscribe: function(channelName, subscription) { - // All subscribes and unsubscribes are synchonized by - // the channel name to prevent inconsistent state - return this._sync(channelName, function() { - return this._syncUnsubscribe(channelName, subscription); - }); - }, - - reset: function() { - this._channels = {}; - this._syncronized = new Synchronized(); - }, - - _sync: function(channelName, fn) { - return this._syncronized.sync(channelName, fn.bind(this)); - }, - - _syncSubscribe: Promise.method(function(name, subscription) { - debug('subscribe: channel=%s', name); - - var existingChannel = this._channels[name]; - - // If the client is resubscribing to an existing channel - // there is no need to re-issue to message to the server - if (existingChannel) { - debug('subscribe: existing: channel=%s', name); - - existingChannel.add(subscription); - return subscription; - } - - return this._onSubscribe(name) - .bind(this) - .then(function(response) { - debug('subscribe: success: channel=%s', name); - - var channel = this._channels[name] = new Channel(name); - channel.add(subscription); - - subscription._subscribeSuccess(response); - - return subscription; - }); - }), - - _syncUnsubscribe: Promise.method(function(name, subscription) { - debug('unsubscribe: channel=%s', name); - var channel = this._channels[name]; - if (!channel) return; - - channel.remove(subscription); - - // Do not perform the `unsubscribe` if the channel is still being used - // by other subscriptions - if (!channel.isUnused()) return; - - delete this._channels[name]; - - return this._onUnsubscribe(name); - }), - - distributeMessage: function(message) { - var channels = Channel.expand(message.channel); - - for (var i = 0, n = channels.length; i < n; i++) { - var channel = this._channels[channels[i]]; - if (channel) { - channel.receive(message.data); - } - } - } - -}; - -module.exports = ChannelSet; diff --git a/libs/halley/lib/protocol/channel.js b/libs/halley/lib/protocol/channel.js deleted file mode 100644 index c4babd6..0000000 --- a/libs/halley/lib/protocol/channel.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -var extend = require('../util/externals').extend; - -var GRAMMAR_CHANNEL_NAME = /^\/[a-zA-Z0-9\-\_\!\~\(\)\$\@]+(\/[a-zA-Z0-9\-\_\!\~\(\)\$\@]+)*$/; -var GRAMMAR_CHANNEL_PATTERN = /^(\/[a-zA-Z0-9\-\_\!\~\(\)\$\@]+)*\/\*{1,2}$/; - -function Channel(name) { - this.id = this.name = name; - this._subscriptions = []; -} - -Channel.prototype = { - add: function(subscription) { - this._subscriptions.push(subscription); - }, - - remove: function(subscription) { - this._subscriptions = this._subscriptions.filter(function(s) { - return s !== subscription; - }); - }, - - receive: function(message) { - var subscriptions = this._subscriptions; - - for (var i = 0; i < subscriptions.length; i++) { - subscriptions[i]._receive(message); - } - }, - - subscribeSuccess: function(response) { - var subscriptions = this._subscriptions; - - for (var i = 0; i < subscriptions.length; i++) { - subscriptions[i]._subscribeSuccess(response); - } - }, - - isUnused: function() { - return !this._subscriptions.length; - } -}; - -/* Statics */ -extend(Channel, { - HANDSHAKE: '/meta/handshake', - CONNECT: '/meta/connect', - SUBSCRIBE: '/meta/subscribe', - UNSUBSCRIBE: '/meta/unsubscribe', - DISCONNECT: '/meta/disconnect', - - META: 'meta', - SERVICE: 'service', - - isValid: function(name) { - return GRAMMAR_CHANNEL_NAME.test(name) || - GRAMMAR_CHANNEL_PATTERN.test(name); - }, - - parse: function(name) { - if (!this.isValid(name)) return null; - return name.split('/').slice(1); - }, - - unparse: function(segments) { - return '/' + segments.join('/'); - }, - - expand: function(name) { - var segments = this.parse(name), - channels = ['/**', name]; - - var copy = segments.slice(); - copy[copy.length - 1] = '*'; - channels.push(this.unparse(copy)); - - for (var i = 1, n = segments.length; i < n; i++) { - copy = segments.slice(0, i); - copy.push('**'); - channels.push(this.unparse(copy)); - } - - return channels; - } - - -}); - -module.exports = Channel; diff --git a/libs/halley/lib/protocol/client.js b/libs/halley/lib/protocol/client.js deleted file mode 100644 index dcf3825..0000000 --- a/libs/halley/lib/protocol/client.js +++ /dev/null @@ -1,525 +0,0 @@ -'use strict'; - -var Extensions = require('./extensions'); -var BayeuxError = require('../util/errors').BayeuxError; -var TransportError = require('../util/errors').TransportError; -var Channel = require('./channel'); -var ChannelSet = require('./channel-set'); -var Dispatcher = require('./dispatcher'); -var Promise = require('bluebird'); -var debug = require('debug')('halley:client'); -var StateMachineMixin = require('../mixins/statemachine-mixin'); -var extend = require('../util/externals').extend; -var Events = require('../util/externals').Events; -var globalEvents = require('../util/global-events'); -var Advice = require('./advice'); -var Subscription = require('./subscription'); -var SubscribeThenable = require('./subscribe-thenable'); - - -var MANDATORY_CONNECTION_TYPES = ['long-polling']; -var DEFAULT_ENDPOINT = '/bayeux'; - -/** - * TODO: make the states/transitions look more like the official client states - * http://docs.cometd.org/reference/bayeux_operation.html#d0e9971 - */ -var FSM = { - name: 'client', - initial: 'UNCONNECTED', - globalTransitions: { - disable: 'DISABLED', - disconnect: 'UNCONNECTED' - }, - transitions: { - // The client is not yet connected - UNCONNECTED: { - connect: 'HANDSHAKING', - reset: 'HANDSHAKING' - }, - // The client is undergoing the handshake process - HANDSHAKING: { - handshakeSuccess: 'CONNECTED', - rehandshake: 'HANDSHAKE_WAIT', // TODO:remove - error: 'HANDSHAKE_WAIT' - }, - // Handshake failed, try again after interval - HANDSHAKE_WAIT: { - timeout: 'HANDSHAKING' - }, - // The client is connected - CONNECTED: { - disconnect: 'DISCONNECTING', - rehandshake: 'HANDSHAKE_WAIT', - reset: 'RESETTING' - }, - // The client is undergoing reset - // RESETTING is handled by the same handler as disconnect, so must - // support the same transitions (with different states) - RESETTING: { - disconnectSuccess: 'HANDSHAKING', - reset: 'HANDSHAKING', - connect: 'HANDSHAKING', - error: 'HANDSHAKING' - }, - // The client is disconnecting - DISCONNECTING: { - disconnectSuccess: 'UNCONNECTED', - reset: 'HANDSHAKING', - connect: 'HANDSHAKING', - error: 'UNCONNECTED' - }, - // The client has been disabled an will not reconnect - // after being sent { advice: none } - DISABLED: { - - } - } -}; - -function validateBayeuxResponse(response) { - if (!response) { - throw new TransportError('No response received'); - } - - if (!response.successful) { - throw new BayeuxError(response.error); - } - - return response; -} - -/** - * The Halley Client - */ -function Client(endpoint, options) { - debug('New client created for %s', endpoint); - if (!options) options = {}; - - var advice = this._advice = new Advice(options); - - debug('Initial advice: %j', this._advice); - - this._extensions = new Extensions(); - this._endpoint = endpoint || DEFAULT_ENDPOINT; - this._channels = new ChannelSet(this._onSubscribe.bind(this), this._onUnsubscribe.bind(this)); - this._dispatcher = options.dispatcher || new Dispatcher(this._endpoint, advice, options); - this._initialConnectionTypes = options.connectionTypes || MANDATORY_CONNECTION_TYPES; - this._messageId = 0; - this._connected = false; - - /** - * How many times have we failed handshaking - */ - this.initStateMachine(FSM); - - this.listenTo(this._dispatcher, 'message', this._receiveMessage); - - this.listenTo(advice, 'advice:handshake', function() { - return this.transitionState('rehandshake', { optional: true, dedup: true }); - }); - - this.listenTo(advice, 'advice:none', function() { - return this.resetTransition('disable', new Error('Client disabled')); - }); - - this.listenTo(this._dispatcher, 'transport:up transport:down', function() { - this._updateConnectionState(); - }); - -} - -Client.prototype = { - addExtension: function(extension) { - this._extensions.add(extension); - }, - - removeExtension: function(extension) { - this._extensions.remove(extension); - }, - - handshake: function() { - debug('handshake'); - return this.transitionState('connect', { optional: true }); - }, - - /** - * Wait for the client to connect - */ - connect: Promise.method(function() { - if (this.stateIs('DISABLED')) throw new Error('Client disabled'); - if (this.stateIs('CONNECTED')) return; - - return this.transitionState('connect', { optional: true }); - }), - - disconnect: Promise.method(function() { - if (this.stateIs('DISABLED')) return; - if (this.stateIs('UNCONNECTED')) return; - - return this.resetTransition('disconnect', new Error('Client disconnected'), { dedup: true }); - }), - - /** - * Returns a thenable of a subscription - */ - subscribe: function(channelName, onMessage, context, onSubscribe) { - var subscription = new Subscription(this._channels, channelName, onMessage, context, onSubscribe); - var subscribePromise = this._channels.subscribe(channelName, subscription); - return new SubscribeThenable(subscribePromise); - }, - - /** - * Publish a message - * @return {Promise} A promise of the response - */ - publish: function(channel, data, options) { - debug('publish: channel=%s, data=%j', channel, data); - - return this.connect() - .bind(this) - .then(function() { - return this._sendMessage({ - channel: channel, - data: data - }, options); - }) - .then(validateBayeuxResponse); - }, - - /** - * Resets the client and resubscribes to existing channels - * This can be used when the client is in an inconsistent state - */ - reset: function() { - debug('reset'); - return this.transitionState('reset', { optional: true }); - }, - - /** - * Returns the clientId or null - */ - getClientId: function() { - return this._dispatcher.clientId; - }, - - listChannels: function() { - return this._channels.getKeys(); - }, - - _onSubscribe: function(channel) { - return this.connect() - .bind(this) - .then(function() { - return this._sendMessage({ - channel: Channel.SUBSCRIBE, - subscription: channel - }); - }) - .then(validateBayeuxResponse); - }, - - _onUnsubscribe: function(channel) { - return this.connect() - .bind(this) - .then(function() { - return this._sendMessage({ - channel: Channel.UNSUBSCRIBE, - subscription: channel - }); - }) - .then(validateBayeuxResponse); - }, - - _updateConnectionState: function() { - // The client is connected when the state - // of the client is CONNECTED and the - // transport is up - var isConnected = this.stateIs('CONNECTED') && this._dispatcher.isTransportUp(); - if (this._connected === isConnected) return; - this._connected = isConnected; - - debug('Connection state changed to %s', isConnected ? 'up' : 'down'); - - this.trigger(isConnected ? 'connection:up' : 'connection:down'); - }, - - onStateChange: function() { - this._updateConnectionState(); - }, - - /** - * The client must issue a handshake with the server - */ - _onEnterHANDSHAKING: function() { - this._dispatcher.clientId = null; - - return this._dispatcher.selectTransport(this._initialConnectionTypes) - .bind(this) - .then(function() { - return this._sendMessage({ - channel: Channel.HANDSHAKE - }, { - attempts: 1 // Note: only try once - }); - }) - .then(function(response) { - validateBayeuxResponse(response); - - this._dispatcher.clientId = response.clientId; - var supportedConnectionTypes = this._supportedTypes = response.supportedConnectionTypes; - - debug('Handshake successful: %s', this._dispatcher.clientId); - - return this._dispatcher.selectTransport(supportedConnectionTypes, true); - }) - .return('handshakeSuccess'); - - }, - - /** - * Handshake has failed. Waits `interval` ms then - * attempts another handshake - */ - _onEnterHANDSHAKE_WAIT: function() { - this._advice.handshakeFailed(); - - var delay = this._advice.getHandshakeInterval(); - - debug('Waiting %sms before rehandshaking', delay); - return Promise.delay(delay) - .return('timeout'); - }, - - /** - * The client has connected. It needs to send out regular connect - * messages. - */ - _onEnterCONNECTED: function() { - this.trigger('connected'); - - // Fire a disconnect when the user navigates away - this.listenTo(globalEvents, 'beforeunload', this.disconnect); - - /* Handshake success, reset count */ - this._advice.handshakeSuccess(); - - this._sendConnect(); - - this._resubscribeAll() // Not chained - .catch(function(err) { - debug('resubscribe all failed on connect: %s', err); - }) - .done(); - - return Promise.resolve(); - }, - - /** - * Stop sending connect messages - */ - _onLeaveCONNECTED: function() { - if (this._connect) { - debug('Cancelling pending connect request'); - this._connect.cancel(); - this._connect = null; - } - - // Fire a disconnect when the user navigates away - this.stopListening(globalEvents); - }, - - /** - * The client will attempt a disconnect and will - * transition back to the HANDSHAKING state - */ - _onEnterRESETTING: function() { - debug('Resetting %s', this._dispatcher.clientId); - return this._onEnterDISCONNECTING(); - }, - - /** - * The client is disconnecting, or resetting. - */ - _onEnterDISCONNECTING: function() { - debug('Disconnecting %s', this._dispatcher.clientId); - - return this._sendMessage({ - channel: Channel.DISCONNECT - }, { - attempts: 1, - timeout: this._advice.getDisconnectTimeout() - }) - .bind(this) - .then(validateBayeuxResponse) - .return('disconnectSuccess') - .finally(function() { - this._dispatcher.close(); - - this.trigger('disconnect'); - }); - }, - - /** - * The client is no longer connected. - */ - _onEnterUNCONNECTED: function() { - this._dispatcher.clientId = null; - this._dispatcher.close(); - debug('Clearing channel listeners for %s', this._dispatcher.clientId); - this._channels.reset(); - }, - - /** - * The server has told the client to go away and - * don't come back - */ - _onEnterDISABLED: function() { - this._onEnterUNCONNECTED(); - this.trigger('disabled'); - }, - - /** - * Use to resubscribe all previously subscribed channels - * after re-handshaking - */ - _resubscribeAll: Promise.method(function() { - var channels = this._channels; - var channelNames = channels.getKeys(); - - return Promise.map(channelNames, function(channelName) { - debug('Client attempting to resubscribe to %s', channelName); - var channel = channels.get(channelName); - - return this._sendMessage({ - channel: Channel.SUBSCRIBE, - subscription: channelName - }) - .then(validateBayeuxResponse) - .tap(function(response) { - channel.subscribeSuccess(response); - }); - - }.bind(this)); - - }), - - /** - * Send a request message to the server, to which a reply should - * be received. - * - * @return Promise of response - */ - _sendMessage: function(message, options) { - message.id = this._generateMessageId(); - - return this._extensions.pipe('outgoing', message) - .bind(this) - .then(function(message) { - if (!message) return; - - return this._dispatcher.sendMessage(message, options) - .bind(this) - .then(function(response) { - return this._extensions.pipe('incoming', response); - }); - }); - - }, - - /** - * Event handler for when a message has been received through a channel - * as opposed to as the result of a request. - */ - _receiveMessage: function(message) { - this._extensions.pipe('incoming', message) - .bind(this) - .then(function(message) { - if (!message) return; - - if (!message || !message.channel || message.data === undefined) return; - this._channels.distributeMessage(message); - return null; - }) - .done(); - }, - - /** - * Generate a unique messageid - */ - _generateMessageId: function() { - this._messageId += 1; - if (this._messageId >= Math.pow(2, 32)) this._messageId = 0; - return this._messageId.toString(36); - }, - - /** - * Periodically fire a connect message with `interval` ms between sends - * Ensures that multiple connect messages are not fired simultaneously. - * - * From the docs: - * The client MUST maintain only a single outstanding connect message. - * If the server does not have a current outstanding connect and a connect - * is not received within a configured timeout, then the server - * SHOULD act as if a disconnect message has been received. - */ - _sendConnect: function() { - if (this._connect) { - debug('Cancelling pending connect request'); - this._connect.cancel(); - this._connect = null; - } - - this._connect = this._sendMessage({ - channel: Channel.CONNECT - }, { - timeout: this._advice.getConnectResponseTimeout() - }) - .bind(this) - .then(validateBayeuxResponse) - .catch(function(err) { - debug('Connect failed: %s', err); - }) - .finally(function() { - this._connect = null; - - // If we're no longer connected so don't re-issue the - // connect again - if (!this.stateIs('CONNECTED')) { - return null; - } - - var interval = this._advice.interval; - - debug('Will connect after interval: %sms', interval); - this._connect = Promise.delay(interval) - .bind(this) - .then(function() { - this._connect = null; - - // No longer connected after the interval, don't re-issue - if (!this.stateIs('CONNECTED')) { - return; - } - - /* Do not chain this */ - this._sendConnect(); - - // Return an empty promise to stop - // bluebird from raising warnings - return Promise.resolve(); - }); - - // Return an empty promise to stop - // bluebird from raising warnings - return Promise.resolve(); - }); - } - -}; - -/* Mixins */ -extend(Client.prototype, Events); -extend(Client.prototype, StateMachineMixin); - -module.exports = Client; diff --git a/libs/halley/lib/protocol/dispatcher.js b/libs/halley/lib/protocol/dispatcher.js deleted file mode 100644 index b581082..0000000 --- a/libs/halley/lib/protocol/dispatcher.js +++ /dev/null @@ -1,298 +0,0 @@ -'use strict'; - -var Scheduler = require('./scheduler'); -var Transport = require('../transport/transport'); -var Channel = require('./channel'); -var TransportPool = require('../transport/pool'); -var uri = require('../util/uri'); -var extend = require('../util/externals').extend; -var Events = require('../util/externals').Events; -var debug = require('debug')('halley:dispatcher'); -var Promise = require('bluebird'); -var Envelope = require('./envelope'); -var TransportError = require('../util/errors').TransportError; -var danglingFinally = require('../util/promise-util').danglingFinally; - -var HANDSHAKE = 'handshake'; -var BAYEUX_VERSION = '1.0'; - -var STATE_UP = 1; -var STATE_DOWN = 2; - -/** - * The dispatcher sits between the client and the transport. - * - * It's responsible for tracking sending messages to the transport, - * tracking in-flight messages - */ -function Dispatcher(endpoint, advice, options) { - this._advice = advice; - this._envelopes = {}; - this._scheduler = options.scheduler || Scheduler; - this._state = 0; - this._pool = new TransportPool(this, uri.parse(endpoint), advice, options.disabled, Transport.getRegisteredTransports()); - -} - -Dispatcher.prototype = { - - destroy: function() { - debug('destroy'); - - this.close(); - }, - - close: function() { - debug('_close'); - - this._cancelPending(); - - debug('Dispatcher close requested'); - this._pool.close(); - }, - - _cancelPending: function() { - var envelopes = this._envelopes; - this._envelopes = {}; - var envelopeKeys = Object.keys(envelopes); - - debug('_cancelPending %s envelopes', envelopeKeys.length); - envelopeKeys.forEach(function(id) { - var envelope = envelopes[id]; - envelope.reject(new Error('Dispatcher closed')); - }, this); - }, - - getConnectionTypes: function() { - return Transport.getConnectionTypes(); - }, - - selectTransport: function(allowedTransportTypes, cleanup) { - return this._pool.setAllowed(allowedTransportTypes, cleanup); - }, - - /** - * Returns a promise of the response - */ - sendMessage: function(message, options) { - var id = message.id; - var envelopes = this._envelopes; - var advice = this._advice; - - var envelope = envelopes[id] = new Envelope(message); - - var timeout; - if (options && options.timeout) { - timeout = options.timeout; - } else { - timeout = advice.timeout; - } - - var scheduler = new this._scheduler(message, { - timeout: timeout, - interval: advice.retry, - attempts: options && options.attempts - }); - - var promise = this._attemptSend(envelope, message, scheduler); - if (options && options.deadline) { - promise = promise.timeout(options && options.deadline, 'Timeout on deadline'); - } - - return danglingFinally(promise, function() { - debug('sendMessage finally: message=%j', message); - - if (promise.isFulfilled()) { - scheduler.succeed(); - } else { - scheduler.abort(); - } - - delete envelopes[id]; - }); - }, - - _attemptSend: function(envelope, message, scheduler) { - if (!scheduler.isDeliverable()) { - return Promise.reject(new Error('No longer deliverable')); - } - - scheduler.send(); - - var timeout = scheduler.getTimeout(); - - // 1. Obtain transport - return this._pool.get() - .bind(this) - .then(function(transport) { - debug('attemptSend: %j', message); - envelope.startSend(transport); - - // 2. Send the message using the given transport - var enrichedMessage = this._enrich(message, transport); - - return transport.sendMessage(enrichedMessage); - }) - .then(function() { - this._triggerUp(); - - // 3. Wait for the response from the transport - return envelope.awaitResponse(); - }) - .timeout(timeout, 'Timeout on message send') - .finally(function() { - envelope.stopSend(); - }) - .then(function(response) { - // 4. Parse the response - - if (response.successful === false && response.advice && response.advice.reconnect === HANDSHAKE) { - // This is not standard, and may need a bit of reconsideration - // but if the client sends a message to the server and the server responds with - // an error and tells the client it needs to rehandshake, - // reschedule the send after the send after the handshake has occurred. - throw new TransportError('Message send failed with advice reconnect:handshake, will reschedule send'); - } - - return response; - }) - .catch(Promise.TimeoutError, TransportError, function(e) { - debug('Error while attempting to send message: %j: %s', message, e); - - this._triggerDown(); - scheduler.fail(); - - if (!scheduler.isDeliverable()) { - throw e; - } - - // Either the send timed out or no transport was - // available. Either way, wait for the interval and try again - return this._awaitRetry(envelope, message, scheduler); - }); - - }, - - /** - * Adds required fields into the message - */ - _enrich: function(message, transport) { - if (message.channel === Channel.CONNECT) { - message.connectionType = transport.connectionType; - } - - if (message.channel === Channel.HANDSHAKE) { - message.version = BAYEUX_VERSION; - message.supportedConnectionTypes = this.getConnectionTypes(); - } else { - if (!this.clientId) { - // Theres probably a nicer way of doing this. If the connection - // is in the process of being re-established, throw an error - // for non-handshake messages which will cause them to be rescheduled - // in future, hopefully once the client is CONNECTED again - throw new Error('client is not yet established'); - } - message.clientId = this.clientId; - } - - return message; - }, - - /** - * Send has failed. Retry after interval - */ - _awaitRetry: function(envelope, message, scheduler) { - // Either no transport is available or a timeout occurred waiting for - // the transport. Wait a bit, the try again - return Promise.delay(scheduler.getInterval()) - .bind(this) - .then(function() { - return this._attemptSend(envelope, message, scheduler); - }); - - }, - - handleResponse: function(reply) { - if (reply.advice) this._advice.update(reply.advice); - var id = reply.id; - var envelope = id && this._envelopes[id]; - - if (reply.successful !== undefined && envelope) { - // This is a response to a message we fired. - envelope.resolve(reply); - } else { - // Distribe this message through channels - // Don't trigger a message if this is a reply - // to a request, otherwise it'll pass - // through the extensions twice - this.trigger('message', reply); - } - }, - - _triggerDown: function() { - if (this._state === STATE_DOWN) return; - debug('Dispatcher is DOWN'); - - this._state = STATE_DOWN; - this.trigger('transport:down'); - }, - - _triggerUp: function() { - if (this._state === STATE_UP) return; - debug('Dispatcher is UP'); - - this._state = STATE_UP; - this.trigger('transport:up'); - - // If we've disable websockets due to a network - // outage, try re-enable them now - this._pool.reevaluate(); - }, - - /** - * Called by transports on connection error - */ - handleError: function(transport) { - // This method may be called from outside the eventloop - // so queue the method to ensure any finally methods - // on pending promises are called prior to executing - Promise.resolve() - .bind(this) - .then(function() { - var envelopes = this._envelopes; - - // If the transport goes down, reject any outstanding - // connect messages. We don't reject non-connect messages - // as we assume that they've been sent to the server - // already, and we don't need to resend them. If they had failed - // to send, the send would have been rejected already. - // As a failback, the message timeout will eventually - // result in a rejection anyway. - Object.keys(envelopes).forEach(function(id) { - var envelope = envelopes[id]; - - var message = envelope.message; - if (envelope.transport === transport && message && message.channel === Channel.CONNECT) { - envelope.reject(new Error('Transport failed')); - } - }); - - // If this transport is the current, - // report the connection as down - if (transport === this._pool.current()) { - this._triggerDown(); - } - - this._pool.down(transport); - }); - }, - - isTransportUp: function() { - return this._state === STATE_UP; - } -}; - -/* Mixins */ -extend(Dispatcher.prototype, Events); - -module.exports = Dispatcher; diff --git a/libs/halley/lib/protocol/envelope.js b/libs/halley/lib/protocol/envelope.js deleted file mode 100644 index cf609fa..0000000 --- a/libs/halley/lib/protocol/envelope.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); - -/** - * An envelope represents a single request and response - * with one or more send requests. - * - * - Sends cannot overlap. - * - When a send is rejected, the dispatcher will queue up - * another send. However the response will only be - * accepted or rejected once - */ -function Envelope() { - this.transport = null; - this._sendPromise = null; - this._sendResolve = null; - this._sendReject = null; -} - -Envelope.prototype = { - - startSend: function(transport) { - this.transport = transport; - this._sendPromise = new Promise(function(resolve, reject) { - this._sendResolve = resolve; - this._sendReject = reject; - }.bind(this)); - - }, - - stopSend: function() { - this.transport = null; - this._sendPromise = null; - this._sendResolve = null; - this._sendReject = null; - }, - - resolve: function(value) { - if (this._sendResolve) { - this._sendResolve(value); - } - }, - - reject: function(reason) { - if (this._sendReject) { - this._sendReject(reason); - } - }, - - awaitResponse: function() { - return this._sendPromise; - } -}; - -module.exports = Envelope; diff --git a/libs/halley/lib/protocol/extensions.js b/libs/halley/lib/protocol/extensions.js deleted file mode 100644 index 0da67c2..0000000 --- a/libs/halley/lib/protocol/extensions.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); - -function Extensions() { - this._extensions = []; -} - -Extensions.prototype = { - add: function(extension) { - this._extensions.push(extension); - }, - - remove: function(extension) { - this._extensions = this._extensions.filter(function(e) { - return e !== extension; - }); - }, - - pipe: function(stage, message) { - var extensions = this._extensions; - - if (!extensions || extensions.length === 0) return Promise.resolve(message); - - extensions = extensions.filter(function(extension) { - return extension && extension[stage]; - }); - - if (!extensions.length) return Promise.resolve(message); - - // Since most of the extensions are synchronous, using - // a callback style iterator tends to be an order - // of magnitude faster than a Promise.reduce style - // function here - return new Promise(function(resolve) { - var index = 0; - (function next(message) { - var current = index++; - if (current >= extensions.length) { - return resolve(message); - } - - var extension = extensions[current]; - var fn = extension[stage]; - fn.call(extension, message, next); - })(message); - }); - } -}; - -module.exports = Extensions; diff --git a/libs/halley/lib/protocol/scheduler.js b/libs/halley/lib/protocol/scheduler.js deleted file mode 100644 index a35032b..0000000 --- a/libs/halley/lib/protocol/scheduler.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -var REPEATED_ATTEMPT_FAILURE_THRESHOLD = 3; -var MIN_INTERVAL_ON_REPEAT_FAILURE = 5000; - -/** - * Handles the scheduling of a single message - */ -function Scheduler(message, options) { - this.message = message; - this.options = options; - this.attempts = 0; - this.failures = 0; - this.finished = false; -} - -Scheduler.prototype = { - getTimeout: function() { - return this.options.timeout; - }, - - getInterval: function() { - if (this.attempts >= REPEATED_ATTEMPT_FAILURE_THRESHOLD) { - return Math.max(this.options.interval, MIN_INTERVAL_ON_REPEAT_FAILURE); - } - return this.options.interval; - }, - - isDeliverable: function() { - if (this.finished) return false; - var allowedAttempts = this.options.attempts; - - if (!allowedAttempts) return true; - - // Say we have 3 attempts... - // On the 3rd failure, it's not deliverable - // On the 3rd attempts, it's deliverable - return this.attempts <= allowedAttempts && this.failures < allowedAttempts; - }, - - /** - * Called immediately prior to resending - */ - send: function() { - this.attempts++; - }, - - /** - * Called when an attempt to send has failed - */ - fail: function() { - this.failures++; - }, - - /** - * Called when the message has been sent successfully - */ - succeed: function() { - this.finished = true; - }, - - /** - * Called when the message is aborted - */ - abort: function() { - this.finished = true; - } -}; - -module.exports = Scheduler; diff --git a/libs/halley/lib/protocol/subscribe-thenable.js b/libs/halley/lib/protocol/subscribe-thenable.js deleted file mode 100644 index 08c6629..0000000 --- a/libs/halley/lib/protocol/subscribe-thenable.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); - -function SubscribeThenable(subscribePromise) { - this._promise = subscribePromise; -} - -SubscribeThenable.prototype = { - then: function(resolve, reject) { - return this._promise.then(resolve, reject); - }, - - catch: function(reject) { - return this._promise.catch(reject); - }, - - bind: function(context) { - return this._promise.bind(context); - }, - - unsubscribe: Promise.method(function() { - if (this.isRejected()) return; - - return this._promise.then(function(subscription) { - return subscription.unsubscribe(); - }); - }), - - // Useful for testing - isPending: function() { - return this._promise.isPending(); - }, - - isFulfilled: function() { - return this._promise.isFulfilled(); - }, - - isRejected: function() { - return this._promise.isRejected(); - } -}; - -module.exports = SubscribeThenable; diff --git a/libs/halley/lib/protocol/subscription.js b/libs/halley/lib/protocol/subscription.js deleted file mode 100644 index 44944dd..0000000 --- a/libs/halley/lib/protocol/subscription.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); - -function Subscription(channels, channel, onMessage, context, onSubscribe) { - this._channels = channels; - this._channel = channel; - this._onMessage = onMessage; - this._context = context; - this._onSubscribe = onSubscribe; -} - -Subscription.prototype = { - - _receive: function(message) { - if (this._onMessage) { - this._onMessage.call(this._context, message); - } - }, - - _subscribeSuccess: function(response) { - if (this._onSubscribe) { - this._onSubscribe.call(this._context, response); - } - }, - - unsubscribe: Promise.method(function() { - return this._channels.unsubscribe(this._channel, this); - }) - -}; - -module.exports = Subscription; diff --git a/libs/halley/lib/transport/base-long-polling-transport.js b/libs/halley/lib/transport/base-long-polling-transport.js deleted file mode 100644 index 713f4b8..0000000 --- a/libs/halley/lib/transport/base-long-polling-transport.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -var Batcher = require('../util/promise-util').Batcher; -var Channel = require('../protocol/channel'); -var debug = require('debug')('halley:batching-transport'); -var inherits = require('inherits'); -var Transport = require('./transport'); -var extend = require('../util/externals').extend; -var TransportError = require('../util/errors').TransportError; - -var MAX_DELAY = 8; - -function findConnectMessage(messages) { - for (var i = 0; i < messages.length; i++) { - var message = messages[i]; - if (message.channel === Channel.CONNECT) return message; - } - - return null; -} - -function BaseLongPollingTransport(dispatcher, endpoint, advice) { - BaseLongPollingTransport.super_.call(this, dispatcher, endpoint, advice); - this._batcher = new Batcher(this._dequeue.bind(this), MAX_DELAY); -} -inherits(BaseLongPollingTransport, Transport); - -extend(BaseLongPollingTransport.prototype, { - close: function() { - this._batcher.destroy(new TransportError('Transport closed')); - }, - - /* Returns a promise of a request */ - sendMessage: function(message) { - var sendImmediate = message.channel === Channel.HANDSHAKE; - - return this._batcher.add(message, sendImmediate); - }, - - _dequeue: function(outbox) { - debug('Flushing batch of %s messages', outbox.length); - - if (outbox.length > 1) { - var connectMessage = findConnectMessage(outbox); - - // If we have sent out a request. don't - // long poll on the response. Instead request - // an immediate response from the server - if (connectMessage) { - connectMessage.advice = { timeout: 0 }; - } - } - - return this.request(outbox); - } - -}); - -module.exports = BaseLongPollingTransport; diff --git a/libs/halley/lib/transport/base-websocket.js b/libs/halley/lib/transport/base-websocket.js deleted file mode 100644 index b5e56aa..0000000 --- a/libs/halley/lib/transport/base-websocket.js +++ /dev/null @@ -1,252 +0,0 @@ -'use strict'; - -var Transport = require('./transport'); -var uri = require('../util/uri'); -var Promise = require('bluebird'); -var debug = require('debug')('halley:websocket'); -var inherits = require('inherits'); -var extend = require('../util/externals').extend; -var globalEvents = require('../util/global-events'); -var TransportError = require('../util/errors').TransportError; - -var WS_CONNECTING = 0; -var WS_OPEN = 1; -var WS_CLOSING = 2; -var WS_CLOSED = 3; - -var PROTOCOLS = { - 'http:': 'ws:', - 'https:': 'wss:' -}; - -var openSocketsCount = 0; - -function getSocketUrl(endpoint) { - endpoint = extend({ }, endpoint); - endpoint.protocol = PROTOCOLS[endpoint.protocol]; - return uri.stringify(endpoint); -} - -function WebSocketTransport(dispatcher, endpoint, advice) { - WebSocketTransport.super_.call(this, dispatcher, endpoint, advice); - - this._pingTimer = null; - this._pingResolves = null; - this._connectPromise = this._createConnectPromise(); -} -inherits(WebSocketTransport, Transport); - -extend(WebSocketTransport.prototype, { - /* Abstract _createWebsocket: function(url) { } */ - - /** - * Connects and returns a promise that resolves when the connection is - * established - */ - connect: function() { - return this._connectPromise || Promise.reject(new TransportError('Socket disconnected')); - }, - - close: function(error) { - /* Only perform close once */ - if (!this._connectPromise) return; - this._connectPromise = null; - openSocketsCount--; - - this._error(error || new TransportError('Websocket transport closed')); - - clearTimeout(this._pingTimer); - - globalEvents.off('network', this._pingNow, this); - globalEvents.off('sleep', this._pingNow, this); - - var socket = this._socket; - if (socket) { - debug('Closing websocket'); - - this._socket = null; - - var state = socket.readyState; - socket.onerror = socket.onclose = socket.onmessage = null; - - if(state === WS_OPEN || state === WS_CONNECTING) { - socket.close(); - } - } - }, - - /* Returns a request */ - request: function(messages) { - return this.connect() - .bind(this) - .then(function() { - var socket = this._socket; - if (!socket || socket.readyState !== WS_OPEN) { - throw new TransportError('Websocket unavailable'); - } - - socket.send(JSON.stringify(messages)); - }) - .catch(function(e) { - this.close(e); - throw e; - }); - }, - - /** - * Returns a promise of a connected socket. - */ - _createConnectPromise: function() { - debug('Entered connecting state, creating new WebSocket connection'); - - var url = getSocketUrl(this.endpoint); - var socket = this._socket = this._createWebsocket(url); - - return new Promise(function(resolve, reject, onCancel) { - if (!socket) { - return reject(new TransportError('Sockets not supported')); - } - - openSocketsCount++; - switch (socket.readyState) { - case WS_OPEN: - resolve(socket); - break; - - case WS_CONNECTING: - break; - - case WS_CLOSING: - case WS_CLOSED: - reject(new TransportError('Socket connection failed')); - return; - } - - socket.onopen = function() { - resolve(socket); - }; - - var self = this; - socket.onmessage = function(e) { - debug('Received message: %s', e.data); - self._onmessage(e); - }; - - socket.onerror = function() { - debug('WebSocket error'); - var err = new TransportError("Websocket error"); - self.close(err); - reject(err); - }; - - socket.onclose = function(e) { - debug('Websocket closed. code=%s reason=%s', e.code, e.reason); - var err = new TransportError("Websocket connection failed: code=" + e.code + ": " + e.reason); - self.close(err); - reject(err); - }; - - onCancel(function() { - debug('Closing websocket connection on cancelled'); - self.close(); - }); - - }.bind(this)) - .bind(this) - .timeout(this._advice.getEstablishTimeout(), 'Websocket connect timeout') - .then(function(socket) { - // Connect success, setup listeners - this._pingTimer = setTimeout(this._pingInterval.bind(this), this._advice.getPingInterval()); - - globalEvents.on('network', this._pingNow, this); - globalEvents.on('sleep', this._pingNow, this); - return socket; - }) - .catch(function(e) { - this.close(e); - throw e; - }); - }, - - _onmessage: function(e) { - var replies = JSON.parse(e.data); - if (!replies) return; - - /* Resolve any outstanding pings */ - if (this._pingResolves) { - this._pingResolves.forEach(function(resolve) { - resolve(); - }); - - this._pingResolves = null; - } - - replies = [].concat(replies); - - this._receive(replies); - }, - - _ping: function() { - debug('ping'); - - return this.connect() - .bind(this) - .then(function(socket) { - // Todo: deal with a timeout situation... - if(socket.readyState !== WS_OPEN) { - throw new TransportError('Socket not open'); - } - - var resolve; - var promise = new Promise(function(res) { - resolve = res; - }); - - var resolvesQueue = this._pingResolves; - if (resolvesQueue) { - resolvesQueue.push(resolve); - } else { - this._pingResolves = [resolve]; - } - - socket.send("[]"); - - return promise; - }) - .timeout(this._advice.getMaxNetworkDelay(), 'Ping timeout') - .catch(function(err) { - this.close(err); - throw err; - }); - }, - - /** - * If we have reason to believe that the connection may be flaky, for - * example, the computer has been asleep for a while, we send a ping - * immediately (don't batch with other ping replies) - */ - _pingNow: function() { - debug('Ping invoked on event'); - this._ping() - .catch(function(err) { - debug('Ping failure: closing socket: %s', err); - }); - }, - - _pingInterval: function() { - this._ping() - .bind(this) - .then(function() { - this._pingTimer = setTimeout(this._pingInterval.bind(this), this._advice.getPingInterval()); - }) - .catch(function(err) { - debug('Interval ping failure: closing socket: %s', err); - }); - } -}); - -WebSocketTransport._countSockets = function() { - return openSocketsCount; -}; - -module.exports = WebSocketTransport; diff --git a/libs/halley/lib/transport/browser/.jshintrc b/libs/halley/lib/transport/browser/.jshintrc deleted file mode 100644 index f5dff8c..0000000 --- a/libs/halley/lib/transport/browser/.jshintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "node": true, - "browser": true, - "unused": true -} diff --git a/libs/halley/lib/transport/browser/browser-websocket.js b/libs/halley/lib/transport/browser/browser-websocket.js deleted file mode 100644 index fafeb68..0000000 --- a/libs/halley/lib/transport/browser/browser-websocket.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -var inherits = require('inherits'); -var extend = require('../../util/externals').extend; -var BaseWebSocket = require('../base-websocket'); - -function BrowserWebSocket(dispatcher, endpoint, advice) { - BrowserWebSocket.super_.call(this, dispatcher, endpoint, advice); -} -inherits(BrowserWebSocket, BaseWebSocket); - -extend(BrowserWebSocket.prototype, { - _createWebsocket: function(url) { - if (window.MozWebSocket) { - return new window.MozWebSocket(url); - } - - if (window.WebSocket) { - return new window.WebSocket(url); - } - }, -}); - - -/* Statics */ -BrowserWebSocket.create = BaseWebSocket.create; -BrowserWebSocket.isUsable = function(/*endpoint*/) { - return window.MozWebSocket || window.WebSocket; -}; - -module.exports = BrowserWebSocket; diff --git a/libs/halley/lib/transport/browser/xhr.js b/libs/halley/lib/transport/browser/xhr.js deleted file mode 100644 index 73e1368..0000000 --- a/libs/halley/lib/transport/browser/xhr.js +++ /dev/null @@ -1,134 +0,0 @@ -'use strict'; - -var BaseLongPollingTransport = require('../base-long-polling-transport'); -var debug = require('debug')('halley:xhr'); -var inherits = require('inherits'); -var extend = require('../../util/externals').extend; -var Promise = require('bluebird'); -var TransportError = require('../../util/errors').TransportError; - -var XML_HTTP_HEADERS_RECEIVED = 2; -var XML_HTTP_LOADING = 3; -var XML_HTTP_DONE = 4; - -var WindowXMLHttpRequest = window.XMLHttpRequest; - -function XHRTransport(dispatcher, endpoint) { - XHRTransport.super_.call(this, dispatcher, endpoint); - this._sameOrigin = isSameOrigin(endpoint); -} -inherits(XHRTransport, BaseLongPollingTransport); - -extend(XHRTransport.prototype, { - encode: function(messages) { - var stringified = JSON.stringify(messages); - if (this._sameOrigin) { - // Same origin requests have proper content-type set, so they - // can use application/json - return stringified; - } else { - // CORS requests are posted as plain text - return 'message=' + encodeURIComponent(stringified); - } - }, - - request: function(messages) { - return new Promise(function(resolve, reject, onCancel) { - var href = this.endpoint.href; - var xhr = new WindowXMLHttpRequest(); - var self = this; - - xhr.open('POST', href, true); - - // Don't set headers for CORS requests - if (this._sameOrigin) { - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('Pragma', 'no-cache'); - } - - function cleanup() { - if (!xhr) return; - xhr.onreadystatechange = null; - xhr = null; - } - - xhr.onreadystatechange = function() { - if (!xhr) return; - - var readyState = xhr.readyState; - - if (readyState !== XML_HTTP_DONE && - readyState !== XML_HTTP_HEADERS_RECEIVED && - readyState !== XML_HTTP_LOADING) return; - - /** - * XMLHTTPRequest implementation in MSXML HTTP (at least in IE 8.0 on Windows XP SP3+) - * does not handle HTTP responses with status code 204 (No Content) properly; - * the `status' property has the value 1223. - */ - var status = xhr.status; - var successful = (status >= 200 && status < 300) || status === 304 || status === 1223; - if (successful) { - resolve(); - } else { - var err = new TransportError('HTTP Status ' + status); - reject(err); - self._error(err); - return; - } - - if (xhr.readyState != XML_HTTP_DONE) { - return; - } - - /* readyState is XML_HTTP_DONE */ - var text = xhr.responseText; - cleanup(); - - var replies; - try { - replies = JSON.parse(text); - } catch (e) { - debug('Unable to parse XHR response: %s', e); - self._error(new TransportError('Server response parse failure')); - return; - } - - if (replies && Array.isArray(replies)) { - self._receive(replies); - } else { - self._error(new TransportError('Invalid response from server')); - } - }; - - xhr.send(this.encode(messages)); - - /* Cancel the XHR request */ - onCancel(function() { - if (!xhr) return; - xhr.onreadystatechange = null; - if (xhr.readyState !== XML_HTTP_DONE) { - xhr.abort(); - } - cleanup(); - }); - - }.bind(this)); - } -}); - -/* Statics */ -XHRTransport.isUsable = function() { - return true; -}; - -function isSameOrigin(uri) { - var location = window.location; - if (!location) return false; - return uri.protocol === location.protocol && - uri.hostname === location.hostname && - uri.port === location.port; -} - - -module.exports = XHRTransport; diff --git a/libs/halley/lib/transport/node/.jshintrc b/libs/halley/lib/transport/node/.jshintrc deleted file mode 100644 index 07282f5..0000000 --- a/libs/halley/lib/transport/node/.jshintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "node": true, - "browser": false, - "unused": true -} diff --git a/libs/halley/lib/transport/node/node-http.js b/libs/halley/lib/transport/node/node-http.js deleted file mode 100644 index 7c7c3c9..0000000 --- a/libs/halley/lib/transport/node/node-http.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; - -var BaseLongPollingTransport = require('../base-long-polling-transport'); -var http = require('http'); -var https = require('https'); -var inherits = require('inherits'); -var extend = require('../../util/externals').extend; -var Promise = require('bluebird'); -var TransportError = require('../../util/errors').TransportError; - -function NodeHttpTransport(dispatcher, endpoint, advice) { - NodeHttpTransport.super_.call(this, dispatcher, endpoint, advice); - - var endpointSecure = this.endpoint.protocol === 'https:'; - this._httpClient = endpointSecure ? https : http; -} -inherits(NodeHttpTransport, BaseLongPollingTransport); - -extend(NodeHttpTransport.prototype, { - encode: function(messages) { - return JSON.stringify(messages); - }, - - request: function(messages) { - - return new Promise(function(resolve, reject, onCancel) { - var self = this; - var content = new Buffer(this.encode(messages), 'utf8'); - var params = this._buildParams(content); - - var request = this._httpClient.request(params, function(response) { - var status = response.statusCode; - var successful = (status >= 200 && status < 300); - if (successful) { - resolve(); - } else { - var err = new TransportError('HTTP Status ' + status); - reject(err); - self._error(err); - return; - } - - var body = ''; - - response.setEncoding('utf8'); - response.on('data', function(chunk) { body += chunk; }); - - response.on('end', function() { - var replies = null; - - try { - replies = JSON.parse(body); - } catch (e) { - self._error(new TransportError('Server response parse failure')); - return; - } - - if (replies && Array.isArray(replies)) { - self._receive(replies); - } else { - self._error(new TransportError('Invalid response from server')); - } - }); - }); - - request.setSocketKeepAlive(true, this._advice.getPingInterval()); - request.once('error', function(error) { - var err = new TransportError(error.message); - reject(err); - self._error(new TransportError('Invalid response from server')); - }); - - request.end(content); - onCancel(function() { - request.abort(); - request.removeAllListeners(); - }); - - }.bind(this)); - }, - - _buildParams: function(content) { - var uri = this.endpoint; - - var params = { - method: 'POST', - host: uri.hostname, - path: uri.path, - headers: { - 'Content-Length': content.length, - 'Content-Type': 'application/json', - 'Host': uri.host - } - }; - - if (uri.port) { - params.port = uri.port; - } - - return params; - } -}); - -/* Statics */ -NodeHttpTransport.isUsable = function(endpoint) { - return (endpoint.protocol === 'http:' || endpoint.protocol === 'https:') && endpoint.host && endpoint.path; -}; - -module.exports = NodeHttpTransport; diff --git a/libs/halley/lib/transport/node/node-websocket.js b/libs/halley/lib/transport/node/node-websocket.js deleted file mode 100644 index 07584d1..0000000 --- a/libs/halley/lib/transport/node/node-websocket.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -var inherits = require('inherits'); -var extend = require('../../util/externals').extend; -var WebSocket = require('faye-websocket'); -var BaseWebSocket = require('../base-websocket'); - -function NodeWebSocket(dispatcher, endpoint, advice) { - NodeWebSocket.super_.call(this, dispatcher, endpoint, advice); -} -inherits(NodeWebSocket, BaseWebSocket); - -extend(NodeWebSocket.prototype, { - _createWebsocket: function(url) { - return new WebSocket.Client(url, [], { extensions: this._dispatcher.wsExtensions }); - }, -}); - - -NodeWebSocket.create = BaseWebSocket.create; -NodeWebSocket.isUsable = function(endpoint) { - return (endpoint.protocol === 'http:' || endpoint.protocol === 'https:') && endpoint.host && endpoint.path; -}; -module.exports = NodeWebSocket; diff --git a/libs/halley/lib/transport/pool.js b/libs/halley/lib/transport/pool.js deleted file mode 100644 index d075e4a..0000000 --- a/libs/halley/lib/transport/pool.js +++ /dev/null @@ -1,185 +0,0 @@ -'use strict'; - -var debug = require('debug')('halley:pool'); -var Promise = require('bluebird'); -var cancelBarrier = require('../util/promise-util').cancelBarrier; -var LazySingleton = require('../util/promise-util').LazySingleton; - -function TransportPool(dispatcher, endpoint, advice, disabled, registered) { - this._dispatcher = dispatcher; - this._endpoint = endpoint; - this._advice = advice; - this._transports = {}; - this._disabled = disabled; - this._registered = registered; - this._current = new LazySingleton(this._reselect.bind(this)); - - this._registeredHash = registered.reduce(function(memo, transport) { - var type = transport[0]; - var Klass = transport[1]; - memo[type] = Klass; - return memo; - }, {}); - - this._allowed = null; - - this.setAllowed(null) - .catch(function(err) { - debug('Unable to preconnect to any available transports: err=%s', err); - }) - .done(); -} - -TransportPool.prototype = { - /** Returns a promise to transport */ - get: function() { - var current = this._current.get(); - return cancelBarrier(current); - }, - - current: function() { - var c = this._current.peek(); - - if (c && c.isFulfilled()) { - return c.value(); - } - }, - - /** - * Set the allowed transport types that `.get` will return - */ - setAllowed: function(allowedTypes, cleanup) { - // Maintain the order from this._allowed - this._allowed = this._registered - .map(function(transport) { - return transport[0]; - }) - .filter(function(type) { - return !allowedTypes || allowedTypes.indexOf(type) >= 0; - }); - - if (cleanup) { - // Remove transports that we won't use - Object.keys(this._transports).forEach(function(type) { - if (this._allowed.indexOf(type) >= 0) return; - - var transport = this._transports[type]; - delete this._transports[type]; - - if (transport.isFulfilled()) { - transport.value().close(); - } else { - transport.cancel(); - } - - }, this); - } - - return this.reevaluate(); - }, - - reevaluate: function() { - this._current.clear(); - var current = this._current.get(); - return cancelBarrier(current); - }, - - _reselect: function() { - var allowed = this._allowed; - debug('_reselect: %j', allowed); - - // Load the transport - var connectionPromises = allowed - .filter(function(type) { - var Klass = this._registeredHash[type]; - - if (this._disabled && this._disabled.indexOf(type) >= 0) return false; - - return Klass.isUsable(this._endpoint); - }, this) - .map(function(type) { - var Klass = this._registeredHash[type]; - - var current = this._transports[type]; - if (current) { - if(!current.isRejected() && !current.isCancelled()) { - return current; - } - // Should we cancel the current? - } - - var instance = new Klass(this._dispatcher, this._endpoint, this._advice); - - var promise = instance.connect ? - instance.connect().return(instance) : - Promise.resolve(instance); - - this._transports[type] = promise; - return promise; - }, this); - - if (!connectionPromises.length) { - return Promise.reject(new Error('No suitable transports available')); - } - - // Return the first usable transport - return Promise.any(connectionPromises) - .then(function(transport) { - debug('Selected transport %s', transport.connectionType); - return transport; - }) - .catch(Promise.AggregateError, function(err) { - /* Fail with the first problem */ - throw err[0]; - }); - }, - - close: function() { - debug('_close'); - - var transports = this._transports; - this._transports = {}; - - var current = this._current.value; - if (current) { - - this._current.clear(); - if (current.isPending()) { - current.cancel(); - } - } - - Object.keys(transports).forEach(function(type) { - var transportPromise = transports[type]; - if (transportPromise.isFulfilled()) { - transportPromise.value().close(); - } else { - transportPromise.cancel(); - } - }); - }, - - /** - * Called on transport close - */ - down: function(transport) { - var connectionType = transport.connectionType; - var transportPromise = this._transports[connectionType]; - if (!transportPromise) return; - - if (transportPromise.isFulfilled()) { - var existingTransport = transportPromise.value(); - if (existingTransport !== transport) return; - - // Don't call transport.close as this - // will be called from the close - delete this._transports[connectionType]; - - // Next time someone does a `.get` we will attempt to reselect - this._current.clear(); - } - } - -}; - -module.exports = TransportPool; diff --git a/libs/halley/lib/transport/transport.js b/libs/halley/lib/transport/transport.js deleted file mode 100644 index 406a107..0000000 --- a/libs/halley/lib/transport/transport.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -var debug = require('debug')('halley:transport'); - -var registeredTransports = []; - -function Transport(dispatcher, endpoint, advice) { - this._dispatcher = dispatcher; - this._advice = advice; - this.endpoint = endpoint; -} - -Transport.prototype = { - close: function() { - }, - - /* Abstract encode: function(messages) { } */ - /* Abstract request: function(messages) { } */ - - /* Returns a promise of a request */ - sendMessage: function(message) { - return this.request([message]); - }, - - _receive: function(replies) { - if (!replies) return; - replies = [].concat(replies); - - debug('Received via %s: %j', this.connectionType, replies); - - for (var i = 0, n = replies.length; i < n; i++) { - this._dispatcher.handleResponse(replies[i]); - } - }, - - _error: function(error) { - this._dispatcher.handleError(this, error); - } - -}; - -/* Statics */ -Transport.getRegisteredTransports = function() { - return registeredTransports; -}; - -Transport.register = function(type, klass) { - registeredTransports.push([type, klass]); - klass.prototype.connectionType = type; -}; - -Transport.getConnectionTypes = function() { - return registeredTransports.map(function(t) { return t[0]; }); -}; - -module.exports = Transport; diff --git a/libs/halley/lib/util/errors.js b/libs/halley/lib/util/errors.js deleted file mode 100644 index 883250c..0000000 --- a/libs/halley/lib/util/errors.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -function defineProperty(obj, name, value) { - Object.defineProperty(obj, name, { - value: value, - configurable: true, - enumerable: false, - writable: true - }); -} - -function BayeuxError(message) { - if (!(this instanceof BayeuxError)) return new BayeuxError(message); - - var code, params, m; - message = message || ''; - var match = /^([\d]+):([^:]*):(.*)$/.exec(message); - - if (match) { - code = parseInt(match[1], 10); - params = match[2].split(','); - m = match[3]; - } - - defineProperty(this, "message", m || message || "bayeuxError"); - defineProperty(this, "name", "BayeuxError"); - defineProperty(this, "code", code); - defineProperty(this, "params", params); - defineProperty(this, "bayeuxMessage", m); - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } else { - Error.call(this); - } -} -BayeuxError.prototype = Object.create(Error.prototype); -BayeuxError.prototype.constructor = BayeuxError; - -function TransportError(message) { - if (!(this instanceof TransportError)) return new TransportError(message); - - defineProperty(this, "message", message); - defineProperty(this, "name", "TransportError"); - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } else { - Error.call(this); - } -} -TransportError.prototype = Object.create(Error.prototype); -TransportError.prototype.constructor = TransportError; - -module.exports = { - BayeuxError: BayeuxError, - TransportError: TransportError -}; diff --git a/libs/halley/lib/util/externals.js b/libs/halley/lib/util/externals.js deleted file mode 100644 index 64870fe..0000000 --- a/libs/halley/lib/util/externals.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -var dependencies = { - use: function(deps) { - Object.keys(deps).forEach(function(key) { - dependencies[key] = deps[key]; - }); - } -}; - -module.exports = dependencies; diff --git a/libs/halley/lib/util/global-events.js b/libs/halley/lib/util/global-events.js deleted file mode 100644 index 492d5a5..0000000 --- a/libs/halley/lib/util/global-events.js +++ /dev/null @@ -1,61 +0,0 @@ -/* jshint browser:true */ -'use strict'; - -var Events = require('../util/externals').Events; -var extend = require('../util/externals').extend; -var debug = require('debug')('halley:global-events'); -// var ReactNative = require('react-native') -// var NetInfo = ReactNative.NetInfo - -var SLEEP_TIMER_INTERVAL = 30000; - -var globalEvents = {}; -extend(globalEvents, Events); - -/* Only install the listener in a browser environment */ -if (typeof window !== 'undefined') { - install(); -} - -installSleepDetector(); - -module.exports = globalEvents; - -function fire(type) { - return function(e) { - debug(type); - globalEvents.trigger(type, e); - }; -} - -function install() { -// window.addEventListener('online', fire('network'), false); -// window.addEventListener('offline', fire('network'), false); - -// var navigatorConnection = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection); - -// if (navigatorConnection) { -// navigatorConnection.addEventListener('typechange', fire('network'), false); -// } -} - -function installSleepDetector() { - var sleepLast = Date.now(); - - var timer = setInterval(function() { - var now = Date.now(); - - if(sleepLast - now > SLEEP_TIMER_INTERVAL * 2) { - /* Sleep detected */ - debug('sleep'); - globalEvents.trigger('sleep'); - } - - sleepLast = now; - }, SLEEP_TIMER_INTERVAL); - - // Unreference the timer in nodejs - if (timer.unref) { - timer.unref(); - } -} diff --git a/libs/halley/lib/util/promise-util.js b/libs/halley/lib/util/promise-util.js deleted file mode 100644 index bb6156f..0000000 --- a/libs/halley/lib/util/promise-util.js +++ /dev/null @@ -1,303 +0,0 @@ - -'use strict'; - -var Promise = require('bluebird'); - -exports.danglingFinally = danglingFinally; -exports.Synchronized = Synchronized; -exports.LazySingleton = LazySingleton; -exports.cancelBarrier = cancelBarrier; -exports.after = after; -exports.Throttle = Throttle; -exports.Batcher = Batcher; -exports.Sequencer = Sequencer; - -/** - * Adds a finally clause not chained to the original - * promise, which allows the `fn` called to use - * reflection methods like `isFulfilled` - * - * Catches any errors to prevent bluebird warnings. - * The other fork of the promise should handle the - * real exception chain - */ -function danglingFinally(promise, fn, context) { - promise.catch(function() {}) - .finally(function() { - fn.call(context); - return null; - }); - - return promise; -} -/** - * Returns a promise which will always resolve after the provided - * promise is no longer pending. Will resolve even if the upstream - * promise is cancelled. - */ -function after(promise) { - if (!promise.isPending()) return Promise.resolve(); - - return new Promise(function(resolve) { - danglingFinally(promise, function() { - return resolve(); - }); - }); -} - -/* Prevent a cancel from propogating upstream */ -function cancelBarrier(promise) { - if (!promise.isPending()) return promise; - - return new Promise(function(resolve, reject) { - return promise.then(resolve, reject); - }); -} - -function LazySingleton(factory) { - this.value = null; - this._factory = factory; -} - -LazySingleton.prototype = { - get: function() { - var value = this.value; - if (value) { - return value; - } - - value = this.value = Promise.try(this._factory); - - return value - .bind(this) - .finally(function() { - if (value !== this.value) return; - - if (!value.isFulfilled()) { - this.value = null; - } - }); - }, - - peek: function() { - return this.value; - }, - - clear: function() { - this.value = null; - } -}; - -function Synchronized() { - this._keys = {}; -} - -Synchronized.prototype = { - sync: function(key, fn) { - var keys = this._keys; - var pending = keys[key]; - - if (pending) { - // Append to the end and wait - pending = keys[key] = after(pending) - .bind(this) - .then(function() { - if (pending === keys[key]) { - delete keys[key]; - } - - return fn(); - }); - } else { - // Execute immediately - pending = keys[key] = Promise.try(fn) - .finally(function() { - if (pending === keys[key]) { - delete keys[key]; - } - }); - } - - return pending; - } -}; - -function Throttle(fn, delay) { - this._fn = fn; - this._delay = delay; - this._next = null; - this._resolveNow = null; - this._rejectNow = null; - this._timer = null; -} - -Throttle.prototype = { - fire: function(forceImmediate) { - if (this._next) { - if (forceImmediate) { - this._resolveNow(); - } - - // Return a fork of the promise - return this._next.tap(function() { }); - } - - var promise = this._next = new Promise(function(resolve, reject) { - this._resolveNow = resolve; - this._rejectNow = reject; - - if (forceImmediate) { - resolve(); - } else { - this._timer = setTimeout(function() { - this._timer = null; - resolve(); - }.bind(this), this._delay); - } - }.bind(this)) - .bind(this) - .finally(this._cleanup) - .then(function() { - return this._fn(); - }); - - // Return a fork of the promise - return promise.tap(function() {}); - }, - - _cleanup: function() { - if (this._timer) { - clearTimeout(this._timer); - this._timer = null; - } - - this._next = null; - this._fireNow = null; - this._rejectNow = null; - }, - - destroy: function(e) { - if (this._rejectNow) { - this._rejectNow(e); - } - this._cleanup(); - } -}; - -function Batcher(fn, delay) { - this._throttle = new Throttle(this._dequeue.bind(this), delay); - this._fn = Promise.method(fn); - this._pending = []; -} - -Batcher.prototype = { - add: function(value, forceImmediate) { - var defer = { value: undefined, promise: undefined }; - - var resolve, reject; - var promise = new Promise(function(res, rej) { - resolve = res; - reject = rej; - }); - - defer.value = value; - defer.promise = promise; - - this._pending.push(defer); - - this._throttle.fire(forceImmediate) - .then(resolve, reject); - - return promise; - }, - - next: function(forceImmediate) { - return this._throttle.fire(forceImmediate); - }, - - _dequeue: function() { - var pending = this._pending; - this._pending = []; - - var values = pending.filter(function(defer) { - return !defer.promise.isCancelled(); - }).map(function(defer) { - return defer.value; - }); - - if (!values.length) return; - - return this._fn(values); - }, - - destroy: function(e) { - this._throttle.destroy(e); - this._pending = []; - } -}; - -/** - * The sequencer will chain a series of promises together - * one after the other. - * - * It will also handle rejections and cancelations - */ -function Sequencer() { - this._queue = []; - this._executing = false; -} - -Sequencer.prototype = { - chain: function(fn) { - var queue = this._queue; - var resolve, reject; - - var promise = new Promise(function(res, rej) { - resolve = res; - reject = rej; - }).then(fn); - - queue.push({ resolve: resolve, reject: reject, promise: promise }); - this._dequeue(); - - return promise; - }, - - _dequeue: function() { - if (this._executing) return; - var queue = this._queue; - - var next = queue.pop(); - if (!next) return; - - next.resolve(); - - this._executing = next.promise; - danglingFinally(next.promise, function() { - this._executing = null; - this._dequeue(); - }, this); - }, - - /** - * Removes all items from the queue and rejects them with - * the supplied error. - * - * Returns a promise which will always resolve once any outstanding - * promises are finalised - */ - clear: function(err) { - var queue = this._queue; - this._queue = []; - - queue.forEach(function(item) { - item.reject(err); - }); - - if (this._executing) { - return after(this._executing); - } else { - return Promise.resolve(); - } - } -}; diff --git a/libs/halley/lib/util/uri.js b/libs/halley/lib/util/uri.js deleted file mode 100644 index 52cbb0b..0000000 --- a/libs/halley/lib/util/uri.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -/* node-safe reference to the window */ -var win = typeof window === 'object' && window; // jshint ignore:line - -module.exports = { - - parse: function(url) { - if (typeof url !== 'string') return url; - var uri = {}, parts, query, pairs, i, n, data; - - var consume = function(name, pattern) { - url = url.replace(pattern, function(match) { - uri[name] = match; - return ''; - }); - uri[name] = uri[name] || ''; - }; - - consume('protocol', /^[a-z]+\:/i); - consume('host', /^\/\/[^\/\?#]+/); - - if (!/^\//.test(url) && !uri.host) - url = win.location.pathname.replace(/[^\/]*$/, '') + url; - - consume('pathname', /^[^\?#]*/); - consume('search', /^\?[^#]*/); - consume('hash', /^#.*/); - - uri.protocol = uri.protocol || win.location.protocol; - - if (uri.host) { - uri.host = uri.host.substr(2); - parts = uri.host.split(':'); - uri.hostname = parts[0]; - uri.port = parts[1] || ''; - } else { - uri.host = win.location.host; - uri.hostname = win.location.hostname; - uri.port = win.location.port; - } - - uri.pathname = uri.pathname || '/'; - uri.path = uri.pathname + uri.search; - - query = uri.search.replace(/^\?/, ''); - pairs = query ? query.split('&') : []; - data = {}; - - for (i = 0, n = pairs.length; i < n; i++) { - parts = pairs[i].split('='); - data[decodeURIComponent(parts[0] || '')] = decodeURIComponent(parts[1] || ''); - } - - uri.query = data; - - uri.href = this.stringify(uri); - return uri; - }, - - stringify: function(uri) { - var string = uri.protocol + '//' + uri.hostname; - if (uri.port) string += ':' + uri.port; - string += uri.pathname + this.queryString(uri.query) + (uri.hash || ''); - return string; - }, - - queryString: function(query) { - var pairs = []; - for (var key in query) { - if (!query.hasOwnProperty(key)) continue; - pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(query[key])); - } - if (pairs.length === 0) return ''; - return '?' + pairs.join('&'); - } -}; diff --git a/libs/halley/package.json b/libs/halley/package.json deleted file mode 100644 index f71bef6..0000000 --- a/libs/halley/package.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "_args": [ - [ - { - "raw": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", - "scope": null, - "escapedName": null, - "name": null, - "rawSpec": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", - "spec": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", - "type": "hosted", - "hosted": { - "type": "github", - "ssh": "git@github.com:terrysahaidak/react-native-halley.git#master", - "sshUrl": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", - "httpsUrl": "git+https://github.com/terrysahaidak/react-native-halley.git#master", - "gitUrl": "git://github.com/terrysahaidak/react-native-halley.git#master", - "shortcut": "github:terrysahaidak/react-native-halley#master", - "directUrl": "https://raw.githubusercontent.com/terrysahaidak/react-native-halley/master/package.json" - } - }, - "/Users/terry/Projects/Gitter/GitterMobile" - ] - ], - "_from": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", - "_id": "halley@0.5.2", - "_inCache": true, - "_installable": true, - "_location": "/halley", - "_phantomChildren": {}, - "_requested": { - "raw": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", - "scope": null, - "escapedName": null, - "name": null, - "rawSpec": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", - "spec": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", - "type": "hosted", - "hosted": { - "type": "github", - "ssh": "git@github.com:terrysahaidak/react-native-halley.git#master", - "sshUrl": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#master", - "httpsUrl": "git+https://github.com/terrysahaidak/react-native-halley.git#master", - "gitUrl": "git://github.com/terrysahaidak/react-native-halley.git#master", - "shortcut": "github:terrysahaidak/react-native-halley#master", - "directUrl": "https://raw.githubusercontent.com/terrysahaidak/react-native-halley/master/package.json" - } - }, - "_requiredBy": [ - "#USER", - "/" - ], - "_resolved": "git+ssh://git@github.com/terrysahaidak/react-native-halley.git#279aa5f20df2d08d27a60e3386b80af8ab9a5fd3", - "_shasum": "1fafc6f92484ed8dd8beff4d0dd0280a0dbf6018", - "_shrinkwrap": null, - "_spec": "git+ssh://git@github.com:terrysahaidak/react-native-halley.git#master", - "_where": "/Users/terry/Projects/Gitter/GitterMobile", - "author": { - "name": "Andrew Newdigate", - "email": "andrew@gitter.im" - }, - "browser": "./browser-standalone", - "bugs": { - "url": "https://github.com/gitterHQ/halley/issues" - }, - "dependencies": { - "backbone": "1.2.3", - "backbone-events-standalone": "git://github.com/suprememoocow/backbone-events-standalone.git#e3cf6aaf0742d655687296753836339dcf0ff483", - "bluebird": "^3.3.1", - "debug": "^2.0.0", - "faye-websocket": "~0.10.0", - "inherits": "^2.0.0", - "lodash": "^3.0.0" - }, - "description": "A bayeux client for modern browsers and node. Forked from Faye", - "devDependencies": { - "coveralls": "^2.11.4", - "express": "^4.13.3", - "gitter-faye": "^1.1.0-h", - "gulp": "^3.9.0", - "gulp-gzip": "^1.2.0", - "gulp-sourcemaps": "^1.6.0", - "gulp-spawn-mocha": "^2.2.1", - "gulp-uglify": "^1.5.1", - "gulp-util": "^3.0.7", - "gulp-webpack": "^1.5.0", - "imports-loader": "^0.6.5", - "internal-ip": "^1.1.0", - "karma": "^0.13.15", - "karma-browserstack-launcher": "^0.1.7", - "karma-chrome-launcher": "^0.2.1", - "karma-firefox-launcher": "^0.1.7", - "karma-mocha": "^0.2.1", - "karma-safari-launcher": "^0.1.1", - "karma-webpack": "^1.7.0", - "lolex": "^1.3.2", - "mocha": "^2.3.4", - "mocha-loader": "^0.7.1", - "node-fetch": "^1.3.3", - "permessage-deflate": ">=0.1.0", - "server-destroy": "^1.0.1", - "setimmediate": "^1.0.4", - "sinon": "^1.12.1", - "sinon-browser-only": "^1.12.1", - "webpack": "^1.12.6", - "webpack-dev-middleware": "^1.2.0", - "whatwg-fetch": "^0.10.0", - "wtfnode": "^0.2.1" - }, - "directories": { - "test": "test" - }, - "engines": { - "node": ">=0.8.0" - }, - "gitHead": "279aa5f20df2d08d27a60e3386b80af8ab9a5fd3", - "homepage": "https://github.com/gitterHQ/halley#readme", - "keywords": [ - "comet", - "websocket", - "pubsub", - "bayeux", - "ajax", - "http" - ], - "license": "MIT", - "main": "./index.js", - "name": "halley", - "optionalDependencies": {}, - "readme": "# Halley\n\n[![Join the chat at https://gitter.im/gitterHQ/halley](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gitterHQ/halley?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/gitterHQ/halley.svg)](https://travis-ci.org/gitterHQ/halley) [![Coverage Status](https://coveralls.io/repos/gitterHQ/halley/badge.svg?branch=master&service=github)](https://coveralls.io/github/gitterHQ/halley?branch=master)\n\nHalley is an experimental fork of James Coglan's excellent Faye library.\n\n## Differences from Faye\n\nThe main differences from Faye are (listed in no particular order):\n* **Uses promises** (and Bluebird's promise cancellation feature) to do the heavy-lifting whereever possible.\n* No Ruby client or server and no server support. Halley is a **Javascript Bayeux client only**\n* **Webpack/browserify packaging**\n* **Client reset support**. This will force the client to rehandshake. This can be useful when the application realises that the connection is dead before the bayeux client does and allows for faster recovery in these situations.\n* **No eventsource support** as we've found them to be unreliable in a ELB/haproxy setup\n* All **durations are in milliseconds**, not seconds\n* Wherever possible, implementations have been replaced with external libraries:\n * Uses [bluebird](https://github.com/petkaantonov/bluebird/) for promises\n * Uses backbone events (or backbone-events-standalone) for events\n * Mocha/sinon/karma for testing\n\n## Why's it called \"Halley\"?\n\nLots of reasons! Halley implements the Bayeux Protocol. The [Bayeux Tapestry](https://en.wikipedia.org/wiki/Bayeux_Tapestry)\ncontains the first know depiction of Halley's Comet. Halley is a [cometd](https://cometd.org) client.\n\n### Usage\n\n### Basic Example\n\n```js\nvar Halley = require('halley');\nvar client = new Halley.Client('/bayeux');\n\nfunction onMessage(message) {\n console.log('Incoming message', message);\n}\n\nclient.subscribe('/channel', onMessage);\n\nclient.publish('/channel2', { value: 1 })\n .then(function(response) {\n console.log('Publish returned', response);\n })\n .catch(function(err) {\n console.error('Publish failed:', err);\n });\n```\n\n### Advanced Example\n\n```js\nvar Halley = require('halley');\nvar Promise = require('bluebird');\n\n/** Create a client (showing the default options) */\nvar client = new Halley.Client('/bayeux', {\n /* The amount of time to wait (in ms) between successive\n * retries on a single message send */\n retry: 30000,\n\n /**\n * An integer representing the minimum period of time, in milliseconds, for a\n * client to delay subsequent requests to the /meta/connect channel.\n * A negative period indicates that the message should not be retried.\n * A client MUST implement interval support, but a client MAY exceed the\n * interval provided by the server. A client SHOULD implement a backoff\n * strategy to increase the interval if requests to the server fail without\n * new advice being received from the server.\n */\n interval: 0,\n\n /**\n * An integer representing the period of time, in milliseconds, for the\n * server to delay responses to the /meta/connect channel.\n * This value is merely informative for clients. Bayeux servers SHOULD honor\n * timeout advices sent by clients.\n */\n timeout: 30000,\n\n /**\n * The maximum number of milliseconds to wait before considering a\n * request to the Bayeux server failed.\n */\n maxNetworkDelay: 30000,\n\n /**\n * The maximum number of milliseconds to wait for a WebSocket connection to\n * be opened. It does not apply to HTTP connections.\n */\n connectTimeout: 30000,\n\n /**\n * Maximum time to wait on disconnect\n */\n disconnectTimeout: 10000\n});\n\nfunction onMessage(message) {\n console.log('Incoming message', message);\n}\n\n/*\n *`.subscribe` returns a thenable with a `.unsubscribe` method\n * but will also resolve as a promise \n */\nvar subscription = client.subscribe('/channel', onMessage);\n\nsubscription\n .then(function() {\n console.log('Subscription successful');\n })\n .catch(function(err) {\n console.log('Subscription failed: ', err);\n });\n\n/** As an example, wait 10 seconds and cancel the subscription */\nPromise.delay(10000)\n .then(function() {\n return subscription.unsubscribe();\n });\n```\n\n\n### Debugging\n\nHalley uses [debug](https://github.com/visionmedia/debug) for debugging.\n\n * To enable in nodejs, `export DEBUG=halley:*`\n * To enable in a browser, `window.localStorage.debug='halley:*'`\n\nTo limit the amount of debug logging produced, you can specify individual categories, eg `export DEBUG=halley:client`.\n\n## Tests\n\nMost of the tests in Halley are end-to-end integration tests, which means running a server environment alongside client tests which run in the browser.\n\nIn order to isolate tests from one another, the server will spawn a new Faye server and Proxy server for each test (and tear them down when the test is complete). \n\nSome of the tests connect to Faye directly, while other tests are performed via the Proxy server which is intended to simulate an reverse-proxy/ELB situation common in many production environments.\n\nThe tests do horrible things in order to test some of the situations we've discovered when using Bayeux and websockets on the web. Examples of things we test to ensure that the client recovers include:\n\n* Corrupting websocket streams, like bad MITM proxies sometimes do\n* Dropping random packets\n* Restarting the server during the test\n* Deleting the client connection from the server during the test\n* Not communicating TCP disconnects from the server-to-client and client-to-server when communicating via the proxy (a situation we've seen on ELB)\n\n## License\n\n(The MIT License)\n\nCopyright (c) 2009-2014 James Coglan and contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the 'Software'), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "readmeFilename": "README.md", - "repository": { - "type": "git", - "url": "git+https://github.com/gitterHQ/halley.git" - }, - "scripts": { - "test": "gulp test" - }, - "version": "0.5.2" -} diff --git a/libs/halley/test/.eslintrc.json b/libs/halley/test/.eslintrc.json deleted file mode 100644 index 8397368..0000000 --- a/libs/halley/test/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "env": { - "node": true, - "browser": true, - "mocha": true - } -} diff --git a/libs/halley/test/.jshintrc b/libs/halley/test/.jshintrc deleted file mode 100644 index b13e059..0000000 --- a/libs/halley/test/.jshintrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "node": true, - "unused": true, - "predef": [ - "describe", - "it", - "before", - "after", - "beforeEach", - "afterEach" - ] -} diff --git a/libs/halley/test/browser-websocket-test.js b/libs/halley/test/browser-websocket-test.js deleted file mode 100644 index 26bd705..0000000 --- a/libs/halley/test/browser-websocket-test.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -var WebSocket = require('../lib/transport/browser/browser-websocket'); -var uri = require('../lib/util/uri'); -var Advice = require('../lib/protocol/advice'); - -describe('browser websocket transport', function() { - beforeEach(function() { - this.dispatcher = { - handleResponse: function() { - }, - handleError: function() { - } - }; - - this.advice = new Advice({ - interval: 0, - timeout: 1000, - retry: 1 - }); - - }); - - describe('direct', function() { - - beforeEach(function() { - this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlDirect), this.advice); - }); - - afterEach(function() { - this.websocket.close(); - }); - - require('./specs/websocket-spec')(); - }); - - describe('proxied', function() { - - beforeEach(function() { - this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlProxied), this.advice); - }); - - afterEach(function() { - this.websocket.close(); - }); - - require('./specs/websocket-server-restart-spec')(); - require('./specs/websocket-bad-connection-spec')(); - }); - -}); diff --git a/libs/halley/test/channel-set-test.js b/libs/halley/test/channel-set-test.js deleted file mode 100644 index 0d22b0f..0000000 --- a/libs/halley/test/channel-set-test.js +++ /dev/null @@ -1,140 +0,0 @@ -'use strict'; - -var ChannelSet = require('../lib/protocol/channel-set'); -var Subscription = require('../lib/protocol/subscription'); -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); - -function settleAll(promises) { - return Promise.all(promises.map(function(promise) { return promise.reflect(); })); -} - -describe('channel-set', function() { - - beforeEach(function() { - this.onSubscribe = sinon.spy(function() { - return Promise.delay(1); - }); - - this.onUnsubscribe = sinon.spy(function() { - return Promise.delay(1); - }); - - this.onSubscribeBadChannel = sinon.spy(function() { - return Promise.delay(1).throw(new Error('Fail')); - }); - - this.onUnsubscribeBadChannel = sinon.spy(function() { - return Promise.delay(1).throw(new Error('Fail')); - }); - - this.sub1 = new Subscription(); - this.sub2 = new Subscription(); - this.sub3 = new Subscription(); - - this.channelSet = new ChannelSet(this.onSubscribe, this.onUnsubscribe); - this.channelSetBadChannel = new ChannelSet(this.onSubscribeBadChannel, this.onUnsubscribeBadChannel); - }); - - it('should subscribe', function() { - return this.channelSet.subscribe('/x', this.sub1) - .bind(this) - .then(function() { - assert(this.onSubscribe.calledWith('/x')); - assert(this.onSubscribe.calledOnce); - - assert.deepEqual(this.channelSet.getKeys(), ['/x']); - }); - }); - - it('should unsubscribe correctly', function() { - return this.channelSet.subscribe('/x', this.sub1) - .bind(this) - .then(function() { - assert(this.onSubscribe.calledWith('/x')); - assert(this.onSubscribe.calledOnce); - assert(this.onUnsubscribe.notCalled); - - return this.channelSet.unsubscribe('/x', this.sub1); - }) - .then(function() { - assert(this.onSubscribe.calledOnce); - assert(this.onUnsubscribe.calledOnce); - assert(this.onUnsubscribe.calledWith('/x')); - }); - }); - - - it('should serialize multiple subscribes that occur in parallel', function() { - return Promise.all([ - this.channelSet.subscribe('/x', this.sub1), - this.channelSet.subscribe('/x', this.sub2) - ]) - .bind(this) - .then(function() { - assert(this.onSubscribe.calledWith('/x')); - assert(this.onSubscribe.calledOnce); - }) - .then(function() { - assert.deepEqual(this.channelSet.getKeys(), ['/x']); - }); - }); - - it('should fail both subscriptions when subscribe occurs in parallel', function() { - var p1 = this.channelSetBadChannel.subscribe('/x', this.sub1); - var p2 = this.channelSetBadChannel.subscribe('/x', this.sub2); - - // Surpress warnings in tests: - p1.catch(function() {}); - p2.catch(function() {}); - - return settleAll([p1, p2]) - .bind(this) - .each(function(x) { - assert(x.isRejected()); - - assert(this.onSubscribeBadChannel.calledWith('/x')); - assert(this.onSubscribeBadChannel.calledTwice); - }) - .then(function() { - assert.deepEqual(this.channelSetBadChannel.getKeys(), []); - }); - }); - - it('should serialize subscribes followed by unsubscribed', function() { - return Promise.all([ - this.channelSet.subscribe('/x', this.sub1), - this.channelSet.unsubscribe('/x', this.sub1), - this.channelSet.subscribe('/x', this.sub2) - ]) - .bind(this) - .then(function() { - assert(this.onSubscribe.calledWith('/x')); - assert(this.onSubscribe.calledTwice); - - assert(this.onUnsubscribe.calledWith('/x')); - assert(this.onUnsubscribe.calledOnce); - - assert.deepEqual(this.channelSet.getKeys(), ['/x']); - }); - }); - - it('should handle parallel subscribes being cancelled', function() { - var s1 = this.channelSet.subscribe('/x', this.sub1); - var s2 = this.channelSet.subscribe('/x', this.sub2); - - s1.cancel(); - - return s2 - .bind(this) - .then(function() { - assert(s1.isCancelled()); - - assert(this.onSubscribe.calledWith('/x')); - assert(this.onSubscribe.calledTwice); - }); - - }); - -}); diff --git a/libs/halley/test/client-all-transports-test.js b/libs/halley/test/client-all-transports-test.js deleted file mode 100644 index 2288cb5..0000000 --- a/libs/halley/test/client-all-transports-test.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -var Halley = require('..'); -var Websocket = require('../lib/transport/base-websocket'); -var assert = require('assert'); - -describe('client-all-transport', function() { - - describe('direct', function() { - - beforeEach(function() { - this.openSocketsBefore = Websocket._countSockets(); - - this.client = new Halley.Client(this.urlDirect, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout - }); - }); - - afterEach(function() { - return this.client.disconnect() - .bind(this) - .then(function() { - // Ensure that all sockets are closed - assert.strictEqual(Websocket._countSockets(), this.openSocketsBefore); - }); - }); - - require('./specs/client-spec')(); - require('./specs/client-bad-websockets-spec')(); - }); - - describe('proxied', function() { - - beforeEach(function() { - this.client = new Halley.Client(this.urlProxied, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout - }); - }); - - afterEach(function() { - return this.client.disconnect() - .then(function() { - // Ensure that all sockets are closed - assert.strictEqual(Websocket._countSockets(), 0); - }); - }); - - require('./specs/client-proxied-spec')(); - - }); - - describe('invalid-endpoint', function() { - beforeEach(function() { - this.client = new Halley.Client(this.urlInvalid, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout - }); - }); - - afterEach(function() { - return this.client.disconnect() - .then(function() { - // Ensure that all sockets are closed - assert.strictEqual(Websocket._countSockets(), 0); - }); - }); - - require('./specs/client-invalid-endpoint-spec')(); - }); - -}); diff --git a/libs/halley/test/client-long-polling-test.js b/libs/halley/test/client-long-polling-test.js deleted file mode 100644 index 958aa53..0000000 --- a/libs/halley/test/client-long-polling-test.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -var Halley = require('..'); - -describe('client-long-polling', function() { - - describe('direct', function() { - - beforeEach(function() { - this.client = new Halley.Client(this.urlDirect, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout, - connectionTypes: ['long-polling'], - disabled: ['websocket', 'callback-polling'] - }); - }); - - afterEach(function() { - return this.client.disconnect(); - }); - - require('./specs/client-spec')(); - - }); - - describe('proxied', function() { - - beforeEach(function() { - this.client = new Halley.Client(this.urlProxied, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout, - connectionTypes: ['long-polling'], - disabled: ['websocket', 'callback-polling'] - }); - }); - - afterEach(function() { - return this.client.disconnect(); - }); - - require('./specs/client-proxied-spec')(); - - }); - - describe('invalid-endpoint', function() { - beforeEach(function() { - this.client = new Halley.Client(this.urlInvalid, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout, - connectionTypes: ['long-polling'], - disabled: ['websocket', 'callback-polling'] - }); - }); - - afterEach(function() { - return this.client.disconnect(); - }); - - require('./specs/client-invalid-endpoint-spec')(); - }); -}); diff --git a/libs/halley/test/client-shutdown-test.js b/libs/halley/test/client-shutdown-test.js deleted file mode 100644 index a40c38e..0000000 --- a/libs/halley/test/client-shutdown-test.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -var fork = require('child_process').fork; -var assert = require('assert'); - -describe('client-shutdown-test', function() { - - it('should cleanup after disconnect on subscribe', function(done) { - var testProcess = fork(__dirname + '/helpers/cleanup-test-process', [this.urlDirect], { - env: { - SUBSCRIBE: 1 - } - }); - - testProcess.on('close', function (code) { - assert.strictEqual(code, 0); - done(); - }); - - }); - - it('should cleanup after disconnect on no messages', function(done) { - var testProcess = fork(__dirname + '/helpers/cleanup-test-process', [this.urlDirect]); - - testProcess.on('close', function (code) { - assert.strictEqual(code, 0); - done(); - }); - - }); - -}); diff --git a/libs/halley/test/client-websockets-test.js b/libs/halley/test/client-websockets-test.js deleted file mode 100644 index 45c2e8b..0000000 --- a/libs/halley/test/client-websockets-test.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -var Halley = require('..'); -var Websocket = require('../lib/transport/base-websocket'); -var assert = require('assert'); - -describe('client-websocket', function() { - describe('direct', function() { - beforeEach(function() { - this.openSocketsBefore = Websocket._countSockets(); - - this.client = new Halley.Client(this.urlDirect, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout, - connectionTypes: ['websocket'], - disabled: ['long-polling', 'callback-polling'] - }); - }); - - afterEach(function() { - return this.client.disconnect() - .bind(this) - .then(function() { - // Ensure that all sockets are closed - assert.strictEqual(Websocket._countSockets(), this.openSocketsBefore); - }); - - }); - - require('./specs/client-spec')(); - }); - - describe('proxied', function() { - beforeEach(function() { - this.client = new Halley.Client(this.urlProxied, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout, - - connectionTypes: ['websocket'], - disabled: ['long-polling', 'callback-polling'] - }); - }); - - afterEach(function() { - return this.client.disconnect() - .then(function() { - // Ensure that all sockets are closed - assert.strictEqual(Websocket._countSockets(), 0); - }); - - }); - - require('./specs/client-proxied-spec')(); - }); - - describe('invalid-endpoint', function() { - beforeEach(function() { - this.client = new Halley.Client(this.urlInvalid, { - retry: this.clientOptions.retry, - timeout: this.clientOptions.timeout, - - connectionTypes: ['websocket'], - disabled: ['long-polling', 'callback-polling'] - }); - }); - - afterEach(function() { - return this.client.disconnect() - .then(function() { - // Ensure that all sockets are closed - assert.strictEqual(Websocket._countSockets(), 0); - }); - }); - - require('./specs/client-invalid-endpoint-spec')(); - }); - -}); diff --git a/libs/halley/test/errors-test.js b/libs/halley/test/errors-test.js deleted file mode 100644 index 7ee0aba..0000000 --- a/libs/halley/test/errors-test.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -var errors = require('../lib/util/errors'); -var Promise = require('bluebird'); -var assert = require('assert'); - -describe('errors', function() { - - it('TransportError should work with bluebird', function() { - return Promise.reject(new errors.TransportError()) - .catch(errors.TransportError, errors.BayeuxError, function() { - }) - .catch(function() { - assert.ok(false); - }); - }); - - it('BayeuxError should work with bluebird', function() { - return Promise.reject(new errors.BayeuxError()) - .catch(errors.TransportError, errors.BayeuxError, function() { - }) - .catch(function() { - assert.ok(false); - }); - }); - -}); diff --git a/libs/halley/test/extensions-test.js b/libs/halley/test/extensions-test.js deleted file mode 100644 index e79028a..0000000 --- a/libs/halley/test/extensions-test.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -var Extensions = require('../lib/protocol/extensions'); -var assert = require('assert'); - -describe('extensions', function() { - - it('should call extensions in sequence', function() { - var count = 0; - - function Ext(x) { - this.incoming = function(message, callback) { - assert.strictEqual(count, 0); - assert.strictEqual(message.count, x - 1); - count++; - - setTimeout(function() { - count--; - message.count = x; - callback(message); - }, 2); - } - } - var extensions = new Extensions(); - - extensions.add(new Ext(1)); - extensions.add(new Ext(2)); - extensions.add(new Ext(3)); - extensions.add(new Ext(4)); - extensions.add(new Ext(5)); - extensions.add(new Ext(6)); - - return extensions.pipe('incoming', { count: 0 }) - .then(function(message) { - assert.deepEqual(message, { count: 6 }); - }); - }); - -}); diff --git a/libs/halley/test/helpers/bayeux-server.js b/libs/halley/test/helpers/bayeux-server.js deleted file mode 100644 index f5c791d..0000000 --- a/libs/halley/test/helpers/bayeux-server.js +++ /dev/null @@ -1,191 +0,0 @@ -/* jshint node:true */ -'use strict'; - -var http = require('http'); -var faye = require('gitter-faye'); -var debug = require('debug')('halley:test:bayeux-server'); -var enableDestroy = require('server-destroy'); -var extend = require('lodash/object/extend'); - -function BayeuxServer() { - this.port = 0; -} - -BayeuxServer.prototype = { - - start: function(callback) { - var server = this.server = http.createServer(); - enableDestroy(server); - - var bayeux = this.bayeux = new faye.NodeAdapter({ - mount: '/bayeux', - timeout: 5, - ping: 2, - engine: { - interval: 0.3 - } - }); - bayeux.attach(server); - - this.publishTimer = setInterval(function() { - bayeux.getClient().publish('/datetime', { date: Date.now() }); - bayeux.getClient().publish('/slow', { date: Date.now() }); - }, 100); - - server.on('upgrade', function(req) { - if (self.crushWebsocketConnections) { - // Really mess things up - req.socket.write(''); - req.socket.destroy(); - } - }); - - var self = this; - - bayeux.addExtension({ - incoming: function(message, req, callback) { - var clientId = message.clientId; - if (!clientId) return callback(message); - - // This is a bit of a hack, but Faye doesn't appear to do it - // automatically: check that the client actually exists. If it - // doesn't reject it - bayeux._server._engine.clientExists(clientId, function(exists) { - if(!exists) { - message.error = '401:' + clientId + ':Unknown client'; - } - - return callback(message); - }); - }, - - outgoing: function(message, req, callback) { - if (message.successful === false && message.error) { - // If we're sending 401 messages to the client, they don't - // actually have a connection, so tell them to rehandshake - if (message.error.indexOf('401:') === 0) { - message.advice = extend(message.advice || {}, { "reconnect": "handshake", interval: 0 }); - } - } - return callback(message); - } - }); - - bayeux.addExtension({ - incoming: function(message, req, callback) { - - if (message.channel === '/meta/subscribe' && message.subscription === '/slow') { - return setTimeout(function() { - callback(message); - }, 200); - } - - return callback(message); - } - - }); - - bayeux.addExtension({ - incoming: function(message, req, callback) { - - if (message.channel === '/meta/handshake' && message.ext && message.ext.deny) { - message.error = '401::Unauthorised'; - } - - return callback(message); - }, - - outgoing: function(message, req, callback) { - if (message.channel === '/meta/handshake' && message.error === '401::Unauthorised') { - message.advice = { reconnect: 'none' }; - } - - return callback(message); - } - - }); - - bayeux.addExtension({ - incoming: function(message, req, callback) { - if (self.crushWebsocketConnections) { - if (req && req.headers.connection === 'Upgrade') { - debug('Disconnecting websocket'); - req.socket.destroy(); - return; - } - } - - if (message.channel === '/meta/subscribe' && message.subscription === '/banned') { - message.error = 'Invalid subscription'; - } - - if (message.channel === '/devnull') { - return; - } - - if (message.channel === '/meta/handshake') { - if (message.ext && message.ext.failHandshake) { - message.error = 'Unable to handshake'; - } - } - - callback(message); - }, - - outgoing: function(message, req, callback) { - var advice; - if (message.channel === '/advice-retry') { - advice = message.advice = message.advice || {}; - advice.reconnect = 'retry'; - advice.timeout = 2000; - } - - if (message.channel === '/advice-handshake') { - advice = message.advice = message.advice || {}; - advice.reconnect = 'handshake'; - advice.interval = 150; - // advice.timeout = 150; - } - - if (message.channel === '/advice-none') { - advice = message.advice = message.advice || {}; - advice.reconnect = 'none'; - } - - return callback(message); - } - }); - - server.listen(this.port, function(err) { - if (err) return callback(err); - self.port = server.address().port; - callback(null, server.address().port); - }); - }, - - stop: function(callback) { - clearTimeout(this.publishTimer); - clearTimeout(this.uncrushTimeout); - this.server.destroy(callback); - this.server = null; - }, - - deleteClient: function(clientId, callback) { - debug('Deleting client', clientId); - this.bayeux._server._engine.destroyClient(clientId, callback); - }, - - crush: function(timeout) { - if (this.crushWebsocketConnections) return; - this.crushWebsocketConnections = true; - this.uncrushTimeout = setTimeout(this.uncrush.bind(this), timeout || 5000); - }, - - uncrush: function() { - if (!this.crushWebsocketConnections) return; - this.crushWebsocketConnections = false; - clearTimeout(this.uncrushTimeout); - } -}; - -module.exports = BayeuxServer; diff --git a/libs/halley/test/helpers/bayeux-with-proxy-server.js b/libs/halley/test/helpers/bayeux-with-proxy-server.js deleted file mode 100644 index 158a6b4..0000000 --- a/libs/halley/test/helpers/bayeux-with-proxy-server.js +++ /dev/null @@ -1,73 +0,0 @@ -/* jshint node:true */ -'use strict'; - -var BayeuxServer = require('./bayeux-server'); -var ProxyServer = require('./proxy-server'); -var Promise = require('bluebird'); - -function BayeuxWithProxyServer(localIp) { - this.localIp = localIp; -} - -BayeuxWithProxyServer.prototype = { - start: function(callback) { - var self = this; - this.bayeuxServer = new BayeuxServer(); - this.bayeuxServer.start(function(err, bayeuxPort) { - if (err) return callback(err); - self.proxyServer = new ProxyServer(bayeuxPort); - self.proxyServer.start(function(err, proxyPort) { - if (err) return callback(err); - - return callback(null, { - bayeux: 'http://' + self.localIp + ':' + bayeuxPort + '/bayeux', - proxied: 'http://' + self.localIp + ':' + proxyPort + '/bayeux', - }); - }); - }); - }, - - stop: function(callback) { - var self = this; - - this.proxyServer.stop(function(err) { - - if (err) return callback(err); - self.bayeuxServer.stop(function(err) { - - if (err) return callback(err); - callback(); - }); - }); - - }, - - networkOutage: Promise.method(function(timeout) { - this.proxyServer.disableTraffic(timeout); - }), - - stopWebsockets: Promise.method(function(timeout) { - this.bayeuxServer.crush(timeout); - }), - - deleteSocket: Promise.method(function(clientId) { - return Promise.fromCallback(this.bayeuxServer.deleteClient.bind(this.bayeuxServer, clientId)); - }), - - restart: Promise.method(function() { - var proxy = this.proxyServer; - return Promise.fromCallback(proxy.stop.bind(proxy)) - .delay(500) - .then(function() { - return Promise.fromCallback(proxy.start.bind(proxy)); - }); - }), - - restoreAll: Promise.method(function() { - this.proxyServer.enableTraffic(); - this.bayeuxServer.uncrush(); - }), - -}; - -module.exports = BayeuxWithProxyServer; diff --git a/libs/halley/test/helpers/cleanup-test-process.js b/libs/halley/test/helpers/cleanup-test-process.js deleted file mode 100644 index 2abcbfa..0000000 --- a/libs/halley/test/helpers/cleanup-test-process.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -var wtf = require('wtfnode'); // Must be first -var Halley = require('../..'); -var Promise = require('bluebird'); - -var url = process.argv[2]; - -var client = new Halley.Client(url); - -function doSubscribe() { - return client.publish('/channel', { data: 1 }) - .then(function() { - var resolve; - var gotMessage = new Promise(function(res) { - resolve = res; - }); - - return [gotMessage, client.subscribe('/datetime', function() { - resolve(); - })]; - }) - .spread(function(message, subscription) { - return subscription.unsubscribe(); - }); -} - -function doNoSubscribe() { - return client.connect() - .then(function() { - return Promise.delay(client._advice.timeout + 1000); - }); -} - -(process.env.SUBSCRIBE ? doSubscribe() : doNoSubscribe()) - .then(function() { - return client.disconnect(); - }) - .then(function() { - client = null; - setInterval(function() { - wtf.dump(); - }, 1000).unref(); - }) - .catch(function(err) { - console.error(err && err.stack || err); - process.exit(1); - }) - .done(); diff --git a/libs/halley/test/helpers/proxy-server.js b/libs/halley/test/helpers/proxy-server.js deleted file mode 100644 index 644259c..0000000 --- a/libs/halley/test/helpers/proxy-server.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict'; - -var net = require('net'); -var debug = require('debug')('halley:test:proxy-server'); -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var enableDestroy = require('server-destroy'); - -function ProxyServer(serverPort) { - EventEmitter.call(this); - this.setMaxListeners(100); - this.serverPort = serverPort; - this.listenPort = 0; -} -util.inherits(ProxyServer, EventEmitter); - -ProxyServer.prototype.start = function(callback) { - var self = this; - var server = this.server = net.createServer(function(c) { //'connection' listener - debug('client connected'); - c.on('end', function() { - debug('client disconnected'); - }); - - self.createClient(c); - }); - - enableDestroy(server); - - server.listen(this.listenPort, function() { //'listening' listener - debug('server bound'); - - self.listenPort = server.address().port; - callback(null, server.address().port); - }); -}; - -ProxyServer.prototype.stop = function(callback) { - this.emit('close'); - this.server.destroy(callback); -}; - -ProxyServer.prototype.createClient = function(incoming) { - var self = this; - debug('connection created'); - - var backend = net.connect({ port: this.serverPort }, function() { - debug('backend connection created'); - - var onClose = function() { - self.removeListener('close', onClose); - incoming.destroy(); - backend.destroy(); - }.bind(this); - - self.on('close', onClose); - - incoming.on('data', function(data) { - if (self.trafficDisabled) { - debug('dropping incoming request'); - return; - } - - backend.write(data); - }); - - backend.on('data', function(data) { - if (self.trafficDisabled) { - debug('dropping backend response'); - return; - } - - incoming.write(data); - }); - - incoming.on('end', function() { - debug('incoming end'); - // Intentionally leave sockets hanging - }); - - backend.on('end', function() { - debug('backend end'); - // Intentionally leave sockets hanging - incoming.destroy(); - }); - - incoming.on('error', function() { - debug('incoming error'); - backend.destroy(); - }); - - backend.on('error', function() { - debug('backend error'); - }); - - backend.on('close', function() { - debug('backend close'); - incoming.destroy(); - }); - - incoming.on('close', function() { - debug('incoming close'); - }); - - }); -}; - -ProxyServer.prototype.disableTraffic = function(timeout) { - debug('Trashing all incoming traffic'); - clearTimeout(this.outageTimer); - this.outageTimer = setTimeout(this.enableTraffic.bind(this), timeout || 5000); - this.trafficDisabled = true; -}; - -ProxyServer.prototype.enableTraffic = function() { - clearTimeout(this.outageTimer); - debug('Re-enabling incoming traffic'); - this.trafficDisabled = false; -}; - - - -module.exports = ProxyServer; diff --git a/libs/halley/test/helpers/public/index.html b/libs/halley/test/helpers/public/index.html deleted file mode 100644 index ee76fb1..0000000 --- a/libs/halley/test/helpers/public/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Halley Tests - - - -
-
- - - diff --git a/libs/halley/test/helpers/remote-server-control.js b/libs/halley/test/helpers/remote-server-control.js deleted file mode 100644 index 224a3af..0000000 --- a/libs/halley/test/helpers/remote-server-control.js +++ /dev/null @@ -1,83 +0,0 @@ -/* jshint browser:true */ - -'use strict'; - -var Promise = require('bluebird'); - -if (!window.Promise) { - window.Promise = Promise; -} - -// Polyfill if required -require('whatwg-fetch'); - -function fetchBluebird(url, options) { - return Promise.resolve(window.fetch(url, options)); -} - -function RemoteServerControl() { - this.urlRoot = HALLEY_TEST_SERVER; - this.id = null; -} - -RemoteServerControl.prototype = { - setup: function() { - return fetchBluebird(this.urlRoot + '/setup', { - method: 'post', - body: "" - }) - .bind(this) - .then(function(response) { - return Promise.resolve(response.json()); - }) - .then(function(json) { - this.id = json.id; - return json.urls; - }); - }, - - teardown: function() { - return fetchBluebird(this.urlRoot + '/control/' + this.id + '/teardown', { - method: 'post', - body: "" - }); - }, - - networkOutage: function(timeout) { - return fetchBluebird(this.urlRoot + '/control/' + this.id + '/network-outage?timeout=' + (timeout || 5000), { - method: 'post', - body: "" - }); - }, - - stopWebsockets: function(timeout) { - return fetchBluebird(this.urlRoot + '/control/' + this.id + '/stop-websockets?timeout=' + (timeout || 5000), { - method: 'post', - body: "" - }); - }, - - deleteSocket: function(clientId) { - return fetchBluebird(this.urlRoot + '/control/' + this.id + '/delete/' + clientId, { - method: 'post', - body: "" - }); - }, - - restart: function() { - return fetchBluebird(this.urlRoot + '/control/' + this.id + '/restart', { - method: 'post', - body: "" - }); - }, - - restoreAll: function() { - return fetchBluebird(this.urlRoot + '/control/' + this.id + '/restore-all', { - method: 'post', - body: "" - }); - }, - -}; - -module.exports = RemoteServerControl; diff --git a/libs/halley/test/helpers/server.js b/libs/halley/test/helpers/server.js deleted file mode 100644 index 3a7dff0..0000000 --- a/libs/halley/test/helpers/server.js +++ /dev/null @@ -1,208 +0,0 @@ -/* jshint node:true */ -'use strict'; - -var http = require('http'); -var express = require('express'); -var webpack = require('webpack'); -var webpackMiddleware = require("webpack-dev-middleware"); -var PUBLIC_DIR = __dirname + '/public'; -var debug = require('debug')('halley:test:server'); -var BayeuxWithProxyServer = require('./bayeux-with-proxy-server'); -var internalIp = require('internal-ip'); -var enableDestroy = require('server-destroy'); - -var localIp = process.env.HALLEY_LOCAL_IP || internalIp.v4(); - -var idCounter = 0; -var servers = {}; -var server; - -var port = process.env.PORT || '8000'; - -function listen(options, callback) { - var app = express(); - server = http.createServer(app); - enableDestroy(server); - - if (options.webpack) { - app.use(webpackMiddleware(webpack({ - context: __dirname + "/..", - entry: "mocha!./test-suite-browser", - output: { - path: __dirname + "/", - filename: "test-suite-browser.js" - }, - resolve: { - alias: { - sinon: 'sinon-browser-only' - } - }, - module: { - noParse: [ - /sinon-browser-only/ - ] - }, - devtool: "#eval", - node: { - console: false, - global: true, - process: true, - Buffer: false, - __filename: false, - __dirname: false, - setImmediate: false - }, - plugins:[ - new webpack.DefinePlugin({ - HALLEY_TEST_SERVER: JSON.stringify('http://' + localIp + ':' + port) - }) - ] - - }), { - noInfo: false, - quiet: false, - - watchOptions: { - aggregateTimeout: 300, - poll: true - }, - - publicPath: "/", - stats: { colors: true } - })); - - app.use(express.static(PUBLIC_DIR)); - } - - app.all('/*', function(req, res, next) { - res.set('Access-Control-Allow-Origin', '*'); - next(); - }); - - app.post('/setup', function(req, res, next) { - var server = new BayeuxWithProxyServer(localIp); - var id = idCounter++; - servers[id] = server; - server.start(function(err, urls) { - if (err) return next(err); - - res.send({ id: id, urls: urls }); - }); - }); - - app.post('/control/:id/teardown', function(req, res, next) { - var id = req.params.id; - - var server = servers[id]; - delete servers[id]; - - server.stop(function(err) { - if (err) return next(err); - res.send('OK'); - }); - }); - - app.post('/control/:id/delete/:clientId', function(req, res, next) { - var id = req.params.id; - var clientId = req.params.clientId; - var server = servers[id]; - - server.deleteSocket(clientId) - .then(function() { - res.send('OK'); - }) - .catch(next); - }); - - - app.post('/control/:id/network-outage', function(req, res, next) { - var id = req.params.id; - var server = servers[id]; - - server.networkOutage() - .then(function() { - res.send('OK'); - }) - .catch(next); - }); - - app.post('/control/:id/restore-all', function(req, res, next) { - var id = req.params.id; - var server = servers[id]; - - server.restoreAll() - .then(function() { - res.send('OK'); - }) - .catch(next); - }); - - app.post('/control/:id/restart', function(req, res, next) { - var id = req.params.id; - var server = servers[id]; - - server.restart() - .then(function() { - res.send('OK'); - }) - .catch(next); - }); - - app.post('/control/:id/stop-websockets', function(req, res, next) { - var id = req.params.id; - var server = servers[id]; - - server.stopWebsockets() - .then(function() { - res.send('OK'); - }) - .catch(next); - }); - - app.use(function(err, req, res, next) { // jshint ignore:line - res.status(500).send(err.message); - }); - - app.get('*', function(req, res) { - res.status(404).send('Not found'); - }); - - server.listen(port, callback); -} - -function unlisten(callback) { - debug('Unlisten'); - Object.keys(servers).forEach(function(id) { - var server = servers[id]; - - server.stop(function(err) { - if (err) { - debug('Error stopping server ' + err); - } - }); - - }); - - servers = {}; - server.destroy(callback); -} - -exports.listen = listen; -exports.unlisten = unlisten; - -if (require.main === module) { - process.on('uncaughtException', function(e) { - console.error(e.stack || e); - process.exit(1); - }); - - listen({ webpack: true }, function(err) { - - if (err) { - debug('Unable to start server: %s', err); - return; - } - - debug('Listening'); - }); -} diff --git a/libs/halley/test/node-websocket-test.js b/libs/halley/test/node-websocket-test.js deleted file mode 100644 index 7acc1f3..0000000 --- a/libs/halley/test/node-websocket-test.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -var WebSocket = require('../lib/transport/node/node-websocket'); -var uri = require('../lib/util/uri'); -var Advice = require('../lib/protocol/advice'); - -describe('node websocket transport', function() { - - beforeEach(function() { - this.dispatcher = { - handleResponse: function() { - }, - handleError: function() { - } - }; - - this.advice = new Advice({ - interval: 0, - timeout: 1000, - retry: 1 - }); - - }); - - describe('direct', function() { - - beforeEach(function() { - this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlDirect), this.advice); - }); - - afterEach(function() { - this.websocket.close(); - }); - - require('./specs/websocket-spec')(); - }); - - describe('proxied', function() { - - beforeEach(function() { - this.websocket = new WebSocket(this.dispatcher, uri.parse(this.urlProxied), this.advice); - }); - - afterEach(function() { - this.websocket.close(); - }); - - require('./specs/websocket-server-restart-spec')(); - require('./specs/websocket-bad-connection-spec')(); - }); - -}); diff --git a/libs/halley/test/on-before-unload-test.js b/libs/halley/test/on-before-unload-test.js deleted file mode 100644 index 8373bde..0000000 --- a/libs/halley/test/on-before-unload-test.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -var Halley = require('..'); -var globalEvents = require('../lib/util/global-events'); - -describe('onbeforeunload', function() { - var client; - - beforeEach(function() { - client = new Halley.Client(this.urlDirect, { timeout: 45 }); - }); - - afterEach(function() { - client.disconnect(); - }); - - it('should respond to beforeunload correctly', function(done) { - var count = 0; - var subscription = client.subscribe('/datetime', function() { - count++; - - if (count === 3) { - client.on('disconnect', done); - globalEvents.trigger('beforeunload'); - } - }); - - subscription.catch(function(err) { - done(err); - }); - - }); - -}); diff --git a/libs/halley/test/promise-util-test.js b/libs/halley/test/promise-util-test.js deleted file mode 100644 index 9f6091b..0000000 --- a/libs/halley/test/promise-util-test.js +++ /dev/null @@ -1,625 +0,0 @@ -'use strict'; - -var promiseUtil = require('../lib/util/promise-util'); -var assert = require('assert'); -var Promise = require('bluebird'); - -describe('promise-util', function() { - describe('Synchronized', function() { - - beforeEach(function() { - this.sync = new promiseUtil.Synchronized(); - }); - - it('should synchronize access with a single item', function() { - return this.sync.sync('1', function() { - return 'a'; - }) - .bind(this) - .then(function(result) { - assert.deepEqual(this.sync._keys, {}); - assert.strictEqual(result, 'a'); - }); - }); - - it('should propogate rejections', function() { - return this.sync.sync('1', function() { - throw new Error('Crash'); - }) - .bind(this) - .then(function() { - assert.ok('Expected failure'); - }, function(err) { - assert.strictEqual(err.message, 'Crash'); - }); - }); - - it('should propogate on queued items', function() { - this.sync.sync('1', function() { return Promise.delay(1).return('a'); }); - return this.sync.sync('1', function() { - return Promise.reject(new Error('Queued error')); - }) - .bind(this) - .then(function() { - assert.ok(false, 'Expected exception'); - }, function(err) { - assert.strictEqual(err.message, 'Queued error'); - }) - .then(function() { - assert.deepEqual(this.sync._keys, {}); - }); - }); - - it('should synchronize access with multiple items', function() { - var count = 0; - return Promise.all([ - this.sync.sync('1', function() { assert.strictEqual(count++, 0); return Promise.delay(2).return('a'); }), - this.sync.sync('1', function() { assert.strictEqual(count++, 1); return 'b'; }) - ]) - .bind(this) - .then(function(result) { - assert.strictEqual(count, 2); - assert.deepEqual(this.sync._keys, {}); - assert.deepEqual(result, ['a', 'b']); - }); - }); - - it('upstream rejections should be isolated', function() { - var count = 0; - - this.sync.sync('1', function() { - return Promise.reject(new Error('Random')); - }).catch(function(err) { - assert(err.message, 'Random'); - count++; - }); - - return this.sync.sync('1', function() { return 'b'; }) - .bind(this) - .then(function(result) { - assert.strictEqual(count, 1); - - assert.deepEqual(this.sync._keys, {}); - assert.deepEqual(result, 'b'); - }); - }); - - it('cancellation should work', function() { - var count = 0; - - var p = this.sync.sync('1', function() { - return new Promise(function(resolve, reject, onCancel) { - Promise.delay(1).then(resolve); - - onCancel(function() { - count++; - }); - }); - }); - - p.cancel(); - - return Promise.delay(2) - .then(function() { - assert.strictEqual(count, 1); - }); - }); - - - it('upstream cancellations should be isolated', function() { - var p1 = this.sync.sync('1', function() { return Promise.delay(3).return('a'); }); - var p2 = this.sync.sync('1', function() { return 'b'; }); - return Promise.delay(1) - .bind(this) - .then(function() { - p1.cancel(); - return p2; - }) - .then(function(result) { - assert.deepEqual(result, 'b'); - assert.deepEqual(this.sync._keys, {}); - }); - }); - - }); - - describe('cancelBarrier', function() { - - it('should propogate resolve', function() { - return promiseUtil.cancelBarrier(Promise.resolve('a')) - .then(function(result) { - assert.strictEqual(result, 'a'); - }); - }); - - it('should propogate reject', function() { - var e = new Error(); - return promiseUtil.cancelBarrier(Promise.reject(e)) - .then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err, e); - }); - }); - - it('should prevent cancellations from propogating past the barrier', function() { - var count = 0; - var resolve; - var p1 = new Promise(function(res, rej, onCancel) { - resolve = res; - onCancel(function() { - count++; - }); - }); - - var p2 = promiseUtil.cancelBarrier(p1) - .then(function(x) { - return x; - }); - - p2.cancel(); - resolve('a'); - return p1 - .then(function(result) { - assert.strictEqual(result, 'a'); - }); - }); - - }); - - describe('after', function() { - - it('should execute after resolve', function() { - return promiseUtil.after(Promise.resolve('a')); - }); - - it('should execute after reject', function() { - var e = new Error(); - var p = Promise.delay(1).throw(e); - p.catch(function() { - // Dangling catch to prevent bluebird warnings - }); - - return promiseUtil.after(p); - }); - - it('should propogate when the source promise is cancelled', function() { - var count = 0; - var resolve; - var p1 = new Promise(function(res, rej, onCancel) { - resolve = res; - onCancel(function() { - count++; - }); - }); - - var p2 = promiseUtil.after(p1) - .then(function() { - assert.strictEqual(count, 1); - }); - - p1.cancel(); - - return p2; - }); - - - it('should execute in sequence', function() { - var count = 0; - var p1 = Promise.resolve('a'); - var p2 = promiseUtil.after(p1).then(function() { assert.strictEqual(count, 0); count++; }); - var p3 = promiseUtil.after(p2).then(function() { assert.strictEqual(count, 1); count++; }); - var p4 = promiseUtil.after(p3).then(function() { assert.strictEqual(count, 2); count++; }); - return p4.then(function() { - assert.strictEqual(count, 3); - }); - }); - }); - - describe('LazySingleton', function() { - - beforeEach(function() { - this.count = 0; - this.lazySingleton = new promiseUtil.LazySingleton(function() { - this.count++; - return this.singletonValue; - }.bind(this)); - - }); - - it('should return a value', function() { - this.singletonValue = Promise.resolve('a'); - return this.lazySingleton.get() - .bind(this) - .then(function(a) { - assert.strictEqual(this.count, 1); - assert.strictEqual(a, 'a'); - }); - }); - - it('should cache the results', function() { - this.singletonValue = Promise.resolve('a'); - return this.lazySingleton.get() - .bind(this) - .then(function(a) { - assert.strictEqual(this.count, 1); - assert.strictEqual(a, 'a'); - return this.lazySingleton.get(); - }) - .then(function(a) { - assert.strictEqual(this.count, 1); - assert.strictEqual(a, 'a'); - }); - }); - - it('should not make multiple calls', function() { - this.singletonValue = Promise.delay(1).return('a'); - return Promise.all([ - this.lazySingleton.get(), - this.lazySingleton.get() - ]) - .bind(this) - .then(function(a) { - assert.strictEqual(this.count, 1); - assert.deepEqual(a, ['a', 'a']); - }); - }); - - it('should handle cancellations', function() { - this.singletonValue = Promise.delay(10).return('a'); - - return Promise.delay(0) - .bind(this) - .then(function() { - assert(this.singletonValue.isPending()); - this.singletonValue.cancel(); - return promiseUtil.after(this.lazySingleton.get()); - }) - .then(function() { - assert.strictEqual(this.count, 1); - - this.singletonValue = Promise.delay(1).return('a'); - return this.lazySingleton.get(); - }) - .then(function(a) { - assert.strictEqual(this.count, 2); - assert.strictEqual(a, 'a'); - }); - - }); - - }); - - describe('Throttle', function() { - beforeEach(function() { - this.count = 0; - this.throwError = false; - this.throttle = new promiseUtil.Throttle(function() { - this.count++; - if (this.throwError) throw new Error('Fail'); - }.bind(this), 10); - - this.slowThrottle = new promiseUtil.Throttle(function() { - this.count++; - }.bind(this), 1000000); - }); - - it('should throttle calls', function() { - return Promise.all([ - this.throttle.fire(), - this.throttle.fire(), - this.throttle.fire() - ]) - .bind(this) - .then(function() { - assert.strictEqual(this.count, 1); - }); - }); - - it('should respect fireImmediate', function() { - return Promise.all([ - this.throttle.fire(), - this.throttle.fire(true), - Promise.delay(10).bind(this).then(function() { - return this.throttle.fire(); - }) - ]) - .bind(this) - .then(function() { - assert.strictEqual(this.count, 2); - }); - }); - - it('should not wait if fireImmediate is called on the first call', function() { - return this.slowThrottle.fire(true) - .bind(this) - .then(function() { - assert.strictEqual(this.count, 1); - }); - }); - - it('should reject on destroy', function() { - var p = this.slowThrottle.fire(); - var e = new Error(); - this.slowThrottle.destroy(e); - return p.then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err, e); - }); - }); - - it('should handle cancellations', function() { - var p = this.throttle.fire(); - return Promise.delay(1) - .bind(this) - .then(function() { - - assert(p.isCancellable()); - p.cancel(); - return Promise.delay(15); - }) - .then(function() { - assert.strictEqual(this.count, 0); - }); - }); - - it('should isolate cancels from one another', function() { - var p = this.throttle.fire(); - var p2 = this.throttle.fire(); - - return Promise.delay(1) - .bind(this) - .then(function() { - assert(p.isCancellable()); - p.cancel(); - return p2; - }) - .then(function() { - assert.strictEqual(this.count, 1); - }); - }); - - it('should cancel the trigger when all fires are cancelled', function() { - var p = this.throttle.fire(); - var p2 = this.throttle.fire(); - - return Promise.delay(1) - .bind(this) - .then(function() { - assert(p.isCancellable()); - assert(p2.isCancellable()); - p.cancel(); - p2.cancel(); - return Promise.delay(15); - }) - .then(function() { - assert.strictEqual(this.count, 0); - }); - }); - - it('should handle rejections', function() { - this.throwError = true; - return this.throttle.fire() - .bind(this) - .then(function() { - assert.ok(false, 'Expected error'); - }, function(err) { - assert.strictEqual(err.message, 'Fail'); - }); - }); - }); - - describe('Batcher', function() { - - beforeEach(function() { - this.count = 0; - this.batcher = new promiseUtil.Batcher(function(items) { - this.count++; - this.items = items; - return 'Hello'; - }.bind(this), 10); - }); - - it('should call on add', function() { - return this.batcher.add(10) - .then(function(value) { - assert.strictEqual(value, 'Hello'); - }); - }); - - it('should call on add with multiple items', function() { - return Promise.all([ - this.batcher.add(1), - this.batcher.add(2), - ]) - .spread(function(a,b) { - assert.strictEqual(a, 'Hello'); - assert.strictEqual(b, 'Hello'); - }); - }); - - it('should return a value', function() { - var p1 = this.batcher.add(1); - var p2 = this.batcher.add(2); - var p3 = this.batcher.add(3); - - assert(p1.isCancellable()); - p1.cancel(); - - return this.batcher.next() - .bind(this) - .then(function() { - assert(p1.isCancelled()); - assert.strictEqual(this.count, 1); - assert.deepEqual(this.items, [2, 3]); - return Promise.all([p2, p3]); - }) - .spread(function(a,b) { - assert.strictEqual(a, 'Hello'); - assert.strictEqual(b, 'Hello'); - }); - }); - - it('should not batch if all the items are cancelled', function() { - var p1 = this.batcher.add(1); - var p2 = this.batcher.add(2); - p1.cancel(); - p2.cancel(); - - return Promise.delay(15) - .bind(this) - .then(function() { - assert.strictEqual(this.count, 0); - }); - }); - - }); - - - describe('Sequencer', function() { - - beforeEach(function() { - this.inPlay = 0; - this.count = 0; - this.sequencer = new promiseUtil.Sequencer(); - this.fn = function() { - this.inPlay++; - this.count++; - var count = this.count; - assert.strictEqual(this.inPlay, 1); - return Promise.delay(1) - .bind(this) - .then(function() { - this.inPlay--; - assert.strictEqual(this.inPlay, 0); - return count; - }); - }.bind(this); - - this.fnReject = function() { - this.count++; - return Promise.delay(1) - .bind(this) - .then(function() { - throw new Error('Fail'); - }); - }.bind(this); - - this.fnWillBeCancelled = function() { - this.count++; - var promise = new Promise(function() {}); - - Promise.delay(1) - .then(function() { - promise.cancel(); - }); - - return promise; - }.bind(this); - - }); - - it('should sequence multiple calls', function() { - return Promise.all([ - this.sequencer.chain(this.fn), - this.sequencer.chain(this.fn) - ]) - .bind(this) - .spread(function(a, b) { - assert.strictEqual(a, 1); - assert.strictEqual(b, 2); - assert.strictEqual(this.count, 2); - }); - }); - - it('should handle rejections', function() { - var promises = [this.sequencer.chain(this.fnReject), this.sequencer.chain(this.fn)]; - return Promise.all(promises.map(function(promise) { - return promise.reflect(); - })) - .bind(this) - .spread(function(a, b) { - assert(a.isRejected()); - - assert.strictEqual(a.reason().message, 'Fail'); - - assert(b.isFulfilled()); - assert.strictEqual(b.value(), 2); - assert.strictEqual(this.count, 2); - return a; - }); - }); - - - it('should handle upstream cancellations', function() { - var p1 = this.sequencer.chain(this.fnWillBeCancelled); - var p2 = this.sequencer.chain(this.fn); - - return p2.bind(this) - .then(function(value) { - assert(p1.isCancelled()); - - assert(p2.isFulfilled()); - assert.strictEqual(value, 2); - assert.strictEqual(this.count, 2); - }); - }); - - it('should handle downstream cancellations', function() { - var count = 0; - - var p1 = this.sequencer.chain(function() { - return Promise.delay(1).then(function() { - count++; - assert.ok(false); - }); - }); - - var p2 = this.sequencer.chain(function() { - assert.strictEqual(count, 0); - }); - - p1.cancel(); - return p2; - }); - - it('should handle the queue being cleared', function() { - var count = 0; - var p1 = this.sequencer.chain(function() { - return Promise.delay(1).then(function() { - count++; - return "a"; - }); - }); - - var p2 = this.sequencer.chain(function() { - return Promise.delay(1).then(function() { - count++; - }); - }); - - p2.catch(function() {}); // Stop warnings - - var err = new Error('Queue cleared'); - - return this.sequencer.clear(err) - .then(function() { - assert.strictEqual(count, 1); - assert(p1.isFulfilled()); - assert.strictEqual(p1.value(), "a"); - - return p2.reflect(); - }) - .then(function(r) { - assert.strictEqual(count, 1); - assert(r.isRejected()); - assert.strictEqual(r.reason(), err); - }); - - }); - - }); - - -}); diff --git a/libs/halley/test/specs/client-advice-spec.js b/libs/halley/test/specs/client-advice-spec.js deleted file mode 100644 index 339fa98..0000000 --- a/libs/halley/test/specs/client-advice-spec.js +++ /dev/null @@ -1,104 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); - -function defer() { - var d = {}; - - d.promise = new Promise(function(resolve, reject) { - d.resolve = resolve; - d.reject = reject; - }); - - return d; -} - -module.exports = function() { - describe('client-advice', function() { - - it('should handle advice retry', function() { - var publishOccurred = false; - var client = this.client; - - var d = defer(); - - return client.subscribe('/datetime', function() { - if (!publishOccurred) return; - d.resolve(); - }) - .then(function() { - return client.publish('/advice-retry', { data: 1 }); - }) - .then(function() { - publishOccurred = true; - }) - .then(function() { - return d.promise; - }) - }); - - /** - * Tests to ensure that after receiving a handshake advice - */ - it('should handle advice handshake', function() { - var client = this.client; - var originalClientId; - var rehandshook = false; - var d = defer(); - - return client.subscribe('/datetime', function() { - if (!rehandshook) return; - d.resolve(); - }) - .then(function() { - originalClientId = client.getClientId(); - - client.once('connected', function() { - rehandshook = true; - }); - - return client.publish('/advice-handshake', { data: 1 }); - }) - .then(function() { - return d.promise; - }) - .then(function() { - assert(client.getClientId()); - assert.notEqual(client.getClientId(), originalClientId); - }); - }); - - /** - * Ensure the client is disabled after advice:none - */ - it('should handle advice none', function() { - var client = this.client; - var d = defer(); - - client.once('disabled', function() { - d.resolve(); - }); - - client.publish('/advice-none', { data: 1 }); - - return d.promise - .then(function() { - assert(client.stateIs('DISABLED')); - - // Don't reconnect - return client.publish('/advice-none', { data: 1 }) - .then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err.message, 'Client disabled'); - - return client.reset(); - }); - }); - - }); - - }); - -}; diff --git a/libs/halley/test/specs/client-bad-connection-spec.js b/libs/halley/test/specs/client-bad-connection-spec.js deleted file mode 100644 index 9f223b0..0000000 --- a/libs/halley/test/specs/client-bad-connection-spec.js +++ /dev/null @@ -1,129 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); - -function defer() { - var d = {}; - - d.promise = new Promise(function(resolve, reject) { - d.resolve = resolve; - d.reject = reject; - }); - - return d; -} -var OUTAGE_TIME = 5000; - -module.exports = function() { - describe('client-bad-connection', function() { - - it('should deal with dropped packets', function() { - var count = 0; - var postOutageCount = 0; - var outageTime; - var outageGraceTime; - var self = this; - - var d = defer(); - this.client.subscribe('/datetime', function() { - count++; - - if (count === 1) { - return self.serverControl.networkOutage(OUTAGE_TIME) - .then(function() { - outageTime = Date.now(); - outageGraceTime = Date.now() + 1000; - }) - .catch(function(err) { - d.reject(err); - }); - } - - if (!outageTime) return; - if (outageGraceTime >= Date.now()) return; - - postOutageCount++; - - if (postOutageCount >= 3) { - assert(Date.now() - outageTime >= (OUTAGE_TIME * 0.8)); - d.resolve(); - } - }) - .then(function() { - return d.promise; - }); - }); - - it('should emit connection events', function() { - var client = this.client; - - var d1 = defer(); - var d2 = defer(); - var d3 = defer(); - - client.once('connection:up', function() { - d1.resolve(); - }); - - return client.connect() - .bind(this) - .then(function() { - // Awaiting initial connection:up - return d1.promise; - }) - .then(function() { - client.once('connection:down', function() { - d2.resolve(); - }); - - return client.subscribe('/datetime', function() {}); - }) - .then(function() { - client.once('connection:up', function() { - d3.resolve(); - }); - - return this.serverControl.restart(); - }) - .then(function() { - // connection:down fired - return d2.promise; - }) - .then(function() { - // connection:up fired - return d3.promise; - }); - }); - - it('should emit connection events without messages', function() { - var client = this.client; - - var d1 = defer(); - client.on('connection:down', function() { - d1.resolve(); - }); - - var d2 = defer(); - return client.connect() - .bind(this) - .delay(400) // Give the connection time to send a connect - .then(function() { - client.on('connection:up', function() { - d2.resolve(); - }); - return this.serverControl.restart(); - }) - .then(function() { - // connection:down fired - return d1.promise; - }) - .then(function() { - // connection:up fired - return d2.promise; - }); - }); - - }); - -}; diff --git a/libs/halley/test/specs/client-bad-websockets-spec.js b/libs/halley/test/specs/client-bad-websockets-spec.js deleted file mode 100644 index 8ee68ae..0000000 --- a/libs/halley/test/specs/client-bad-websockets-spec.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); - -function defer() { - var d = {}; - - d.promise = new Promise(function(resolve, reject) { - d.resolve = resolve; - d.reject = reject; - }); - - return d; -} - -var OUTAGE_TIME = 2000; - -module.exports = function() { - describe('client-bad-websockets', function() { - - it('should deal with bad corporate proxies', function() { - var count = 0; - var self = this; - - var d = defer(); - - return this.serverControl.stopWebsockets(OUTAGE_TIME) - .then(function() { - return self.client.subscribe('/datetime', function() { - count++; - - if (count === 3) { - d.resolve(); - } - }); - }) - .then(function() { - return d.promise; - }); - - - }); - - - }); - -}; diff --git a/libs/halley/test/specs/client-connect-spec.js b/libs/halley/test/specs/client-connect-spec.js deleted file mode 100644 index 0682b9a..0000000 --- a/libs/halley/test/specs/client-connect-spec.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); - -module.exports = function() { - describe('client-connect', function() { - - it('should not timeout on empty connect messages', function() { - var client = this.client; - var connectionWentDown = false; - client.on('connection:down', function() { - connectionWentDown = true; - }); - - return client.connect() - .then(function() { - return Promise.delay(client._advice.timeout + 1000); - }) - .then(function() { - assert(!connectionWentDown); - }); - }); - - describe('should not try reconnect when denied access', function() { - beforeEach(function() { - this.extension = { - outgoing: function(message, callback) { - if (message.channel === '/meta/handshake') { - message.ext = { deny: true }; - } - callback(message); - } - }; - - this.client.addExtension(this.extension); - }); - - afterEach(function() { - this.client.removeExtension(this.extension); - }); - - it('should disconnect', function() { - var client = this.client; - - return client.connect() - .then(function() { - assert.ok(false); - }, function(e) { - assert.strictEqual(e.message, 'Unauthorised'); - }); - }); - }); - - }); - -}; diff --git a/libs/halley/test/specs/client-delete-spec.js b/libs/halley/test/specs/client-delete-spec.js deleted file mode 100644 index 6141d95..0000000 --- a/libs/halley/test/specs/client-delete-spec.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); - -function defer() { - var d = {}; - - d.promise = new Promise(function(resolve, reject) { - d.resolve = resolve; - d.reject = reject; - }); - - return d; -} - -module.exports = function() { - describe('client-delete', function() { - /** - * This test ensures that the client is able to recover from a situation - * where the server unexpectedly deletes the client and the client - * no longer exists on the server - */ - it('should recover from an unexpected disconnect', function() { - var client = this.client; - var count = 0; - var deleteOccurred = false; - var originalClientId; - var serverControl = this.serverControl; - - var d = defer(); - return client.subscribe('/datetime', function() { - if (!deleteOccurred) return; - count++; - if (count === 3) { - d.resolve(); - } - }).then(function() { - originalClientId = client.getClientId(); - assert(originalClientId); - - return serverControl.deleteSocket(client.getClientId()); - }) - .then(function() { - deleteOccurred = true; - }) - .then(function() { - return d.promise; - }) - .then(function() { - assert.notEqual(originalClientId, client.getClientId()); - }); - }); - - - }); -}; diff --git a/libs/halley/test/specs/client-invalid-endpoint-spec.js b/libs/halley/test/specs/client-invalid-endpoint-spec.js deleted file mode 100644 index 4f6c644..0000000 --- a/libs/halley/test/specs/client-invalid-endpoint-spec.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); - -module.exports = function() { - describe('client-invalid-endpoint', function() { - - it('should not connect', function() { - var client = this.client; - var connectionCameUp = false; - client.on('connection:down', function() { - connectionCameUp = true; - }); - - return Promise.any([client.connect(), Promise.delay(5000)]) - .then(function() { - assert(!connectionCameUp); - return client.disconnect(); - }); - }); - - }); - -}; diff --git a/libs/halley/test/specs/client-proxied-spec.js b/libs/halley/test/specs/client-proxied-spec.js deleted file mode 100644 index 8963542..0000000 --- a/libs/halley/test/specs/client-proxied-spec.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -module.exports = function() { - require('./client-server-restart-spec')(); - require('./client-bad-connection-spec')(); -}; diff --git a/libs/halley/test/specs/client-publish-spec.js b/libs/halley/test/specs/client-publish-spec.js deleted file mode 100644 index 0ac1864..0000000 --- a/libs/halley/test/specs/client-publish-spec.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); -var assert = require('assert'); - -module.exports = function() { - - describe('client-publish', function() { - - it('should handle publishes', function() { - var client = this.client; - - return client.publish('/channel', { data: 1 }); - }); - - it('should fail when a publish does not work', function() { - return this.client.publish('/devnull', { data: 1 }, { attempts: 1 }) - .then(function() { - throw new Error('Expected failure'); - }, function() { - // Swallow the error - }); - - }); - - it('should handle a large number of publish messages', function() { - var count = 0; - var self = this; - return (function next() { - count++; - if (count >= 10) return; - - return self.client.publish('/channel', { data: count }) - .then(function() { - return next(); - }); - })(); - }); - - it('should handle a parallel publishes', function() { - var count = 0; - var self = this; - return (function next() { - count++; - if (count >= 20) return; - - return Promise.all([ - self.client.publish('/channel', { data: count }), - self.client.publish('/channel', { data: count }), - self.client.publish('/channel', { data: count }), - ]) - .then(function() { - return next(); - }); - })(); - }); - - it('should handle the cancellation of one publish without affecting another', function() { - var p1 = this.client.publish('/channel', { data: 1 }) - .then(function() { - throw new Error('Expected error'); - }); - - var p2 = this.client.publish('/channel', { data: 2 }); - p1.cancel(); - - return p2.then(function(v) { - assert(v.successful); - assert(p1.isCancelled()); - assert(p2.isFulfilled()); - }); - - }); - - - }); - -}; diff --git a/libs/halley/test/specs/client-reset-spec.js b/libs/halley/test/specs/client-reset-spec.js deleted file mode 100644 index b30cc94..0000000 --- a/libs/halley/test/specs/client-reset-spec.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); - -function defer() { - var d = {}; - - d.promise = new Promise(function(resolve, reject) { - d.resolve = resolve; - d.reject = reject; - }); - - return d; -} - -module.exports = function() { - describe('client-reset', function() { - it('a reset should proceed normally', function() { - var client = this.client; - var originalClientId; - var rehandshook = false; - var count = 0; - var postResetCount = 0; - var d = defer(); - - return client.subscribe('/datetime', function() { - count++; - if (count === 1) { - originalClientId = client.getClientId(); - assert(originalClientId); - client.reset(); - - client.once('connected', function() { - rehandshook = true; - }); - - return; - } - - if (rehandshook) { - postResetCount++; - - // Wait for two messages to arrive after the reset to avoid - // the possiblity of a race condition in which a message - // arrives at the same time as the reset - if (postResetCount > 3) { - d.resolve(); - } - } - }) - .then(function() { - return d.promise; - }) - .then(function() { - assert(client.getClientId()); - assert(client.getClientId() !== originalClientId); - }); - }); - - - }); - -}; diff --git a/libs/halley/test/specs/client-server-restart-spec.js b/libs/halley/test/specs/client-server-restart-spec.js deleted file mode 100644 index f2fdc77..0000000 --- a/libs/halley/test/specs/client-server-restart-spec.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); - -function defer() { - var d = {}; - - d.promise = new Promise(function(resolve, reject) { - d.resolve = resolve; - d.reject = reject; - }); - - return d; -} - -module.exports = function() { - describe('client-server-restart', function() { - it('should deal with a server restart', function() { - var client = this.client; - var count = 0; - var postOutageCount = 0; - var outageTime; - var clientId; - var d = defer(); - var serverControl = this.serverControl; - - return client.subscribe('/datetime', function() { - count++; - - if (count === 3) { - clientId = client.getClientId(); - - return serverControl.restart() - .then(function() { - outageTime = Date.now(); - }) - .catch(function(err) { - d.reject(err); - }); - } - - if (!outageTime) return; - - postOutageCount++; - - if (postOutageCount >= 3) { - d.resolve(); - } - }).then(function() { - return d.promise; - }) - .then(function() { - // A disconnect should not re-initialise the client - assert.strictEqual(clientId, client.getClientId()); - }); - }); - - }); -}; diff --git a/libs/halley/test/specs/client-spec.js b/libs/halley/test/specs/client-spec.js deleted file mode 100644 index d368fa4..0000000 --- a/libs/halley/test/specs/client-spec.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function() { - require('./client-connect-spec')(); - require('./client-subscribe-spec')(); - require('./client-publish-spec')(); - require('./client-reset-spec')(); - require('./client-advice-spec')(); - require('./client-delete-spec')(); - require('./client-bad-websockets-spec')(); -}; diff --git a/libs/halley/test/specs/client-subscribe-spec.js b/libs/halley/test/specs/client-subscribe-spec.js deleted file mode 100644 index 40c96bc..0000000 --- a/libs/halley/test/specs/client-subscribe-spec.js +++ /dev/null @@ -1,248 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Promise = require('bluebird'); -var errors = require('../../lib/util/errors'); - -function defer() { - var d = {}; - - d.promise = new Promise(function(resolve, reject) { - d.resolve = resolve; - d.reject = reject; - }); - - return d; -} - -module.exports = function() { - describe('client-subscribe', function() { - - it('should subscribe to a channel and receive messages', function() { - var onMessage = defer(); - var onSubscribe = defer(); - - var subscription = this.client.subscribe('/datetime', function() { - onMessage.resolve(); - }, null, function() { - onSubscribe.resolve(); - }); - - return Promise.all([subscription, onMessage.promise, onSubscribe.promise]); - }); - - it('should unsubscribe a subscription correctly', function() { - var count = 0; - var onSubscribeCount = 0; - var d = defer(); - - var subscribe = this.client.subscribe('/datetime', function() { - count++; - if (count === 2) { - d.resolve(); - } - }, null, function() { - onSubscribeCount++; - }); - - return Promise.join(subscribe, d.promise, function(subscription) { - return subscription.unsubscribe(); - }) - .bind(this) - .then(function() { - assert.strictEqual(onSubscribeCount, 1); - assert.deepEqual(this.client.listChannels(), []); - }); - }); - - it('should handle subscriptions that are cancelled before establishment, single', function() { - var onSubscribeCount = 0; - - var subscribe = this.client.subscribe('/datetime', function() { - assert.ok(false); - }, null, function() { - - }); - - return this.client.connect() - .bind(this) - .then(function() { - // The subscribe will be inflight right now - assert(subscribe.isPending()); - return subscribe.unsubscribe(); - }) - .then(function() { - assert.strictEqual(onSubscribeCount, 0); - assert.deepEqual(this.client.listChannels(), []); - }); - }); - - it('should handle subscriptions that are cancelled before establishment, double', function() { - var onSubscribe = 0; - - var subscribe1 = this.client.subscribe('/datetime', function() { - assert.ok(false); - }, null, function() { - onSubscribe++; - }); - - var d = defer(); - - var subscribe2 = this.client.subscribe('/datetime', d.resolve, null, function() { - onSubscribe++; - }); - - return this.client.connect() - .bind(this) - .then(function() { - - // The subscribe will be inflight right now - assert(subscribe1.isPending()); - return subscribe1.unsubscribe(); - }) - .then(function() { - return subscribe2; - }) - .then(function() { - assert.strictEqual(onSubscribe, 1); - assert.deepEqual(this.client.listChannels(), ['/datetime']); - return d.promise; - }); - }); - - it('should handle subscriptions that are cancelled before establishment, then resubscribed', function() { - var subscription = this.client.subscribe('/datetime', function() { }); - - var promise = this.client.connect() - .bind(this) - .then(function() { - return subscription.unsubscribe(); - }) - .then(function() { - var d = defer(); - return Promise.all([this.client.subscribe('/datetime', d.resolve), d.promise]); - }) - .then(function() { - assert.deepEqual(this.client.listChannels(), ['/datetime']); - }); - - // Cancel immediately - subscription.unsubscribe(); - - return promise; - }); - - it('should handle subscription failure correctly', function() { - return this.client.subscribe('/banned', function() { - assert(false); - }) - .then(function() { assert(false); }, function() { }); - }); - - it('should handle subscribe then followed by catch', function() { - return this.client.subscribe('/banned', function() { - assert(false); - }) - .then(function() { - assert(false); - }) - .catch(function(err) { - assert(err instanceof errors.BayeuxError); - assert.strictEqual(err.message, 'Invalid subscription'); - }); - }); - - it('should handle subscribe with catch', function() { - var count = 0; - return this.client.subscribe('/banned', function() { - assert(false); - }) - .catch(function(err) { - assert(err instanceof errors.BayeuxError); - assert.strictEqual(err.message, 'Invalid subscription'); - count++; - }) - .then(function() { - assert.strictEqual(count, 1); - }); - }); - - it('should deal with subscriptions that fail with unknown client', function() { - return this.client.connect() - .bind(this) - .then(function() { - return this.serverControl.deleteSocket(this.client.getClientId()) - .delay(100); // Give the server time to disconnect - }) - .then(function() { - var d = defer(); - - var subscribe = this.client.subscribe('/datetime', d.resolve); - return Promise.all([subscribe, d.promise]); - }); - - }); - - it('cancelling one subscription during handshake should not affect another', function() { - var subscribe1 = this.client.subscribe('/datetime', function() { }); - var subscribe2 = this.client.subscribe('/datetime', function() { }); - - return Promise.delay(1) - .bind(this) - .then(function() { - assert(subscribe1.isPending()); - return [subscribe1.unsubscribe(), subscribe2]; - }) - .spread(function(unsubscribe, subscription2) { - assert.deepEqual(this.client.listChannels(), ['/datetime']); - return subscription2.unsubscribe(); - }) - .then(function() { - assert.deepEqual(this.client.listChannels(), []); - }); - }); - - it('unsubscribing from a channel after a disconnect should not reconnect the client', function() { - return this.client.subscribe('/datetime', function() { }) - .bind(this) - .then(function(subscription) { - return this.client.disconnect() - .bind(this) - .then(function() { - assert(this.client.stateIs('UNCONNECTED')); - return subscription.unsubscribe(); - }) - .then(function() { - assert(this.client.stateIs('UNCONNECTED')); - }); - }); - }); - - describe('extended tests #slow', function() { - - it('should handle multiple subscribe/unsubscribes', function() { - var i = 0; - var client = this.client; - return (function next() { - if (++i > 15) return; - - var subscribe = client.subscribe('/datetime', function() { }); - - return (i % 2 === 0 ? subscribe : Promise.delay(1)) - .then(function() { - return subscribe.unsubscribe(); - }) - .then(function() { - assert.deepEqual(client.listChannels(), []); - }) - .then(next); - })() - .then(function() { - assert.deepEqual(client.listChannels(), []); - }); - }); - }); - - }); - -}; diff --git a/libs/halley/test/specs/websocket-bad-connection-spec.js b/libs/halley/test/specs/websocket-bad-connection-spec.js deleted file mode 100644 index 98d5d0f..0000000 --- a/libs/halley/test/specs/websocket-bad-connection-spec.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -var Promise = require('bluebird'); -var globalEvents = require('../../lib/util/global-events'); - -module.exports = function() { - describe('websocket-bad-connection', function() { - - it('should terminate if the server cannot be pinged', function() { - var serverControl = this.serverControl; - - return this.websocket.connect() - .bind(this) - .then(function() { - var self = this; - return Promise.all([ - new Promise(function(resolve) { - self.dispatcher.handleError = function() { - resolve(); - } - }), - serverControl.networkOutage(2000) - ]); - }); - }); - - /** - * This test simulates a network event, such as online/offline detection - * This should make the speed of recovery much faster - */ - it('should terminate if the server cannot be pinged after a network event', function() { - var serverControl = this.serverControl; - - return this.websocket.connect() - .bind(this) - .then(function() { - var self = this; - return Promise.all([ - new Promise(function(resolve) { - self.dispatcher.handleError = function() { - resolve(); - } - }), - serverControl.networkOutage(2000) - .then(function() { - globalEvents.trigger('network'); - }) - ]); - }); - }); - - }); -}; diff --git a/libs/halley/test/specs/websocket-server-restart-spec.js b/libs/halley/test/specs/websocket-server-restart-spec.js deleted file mode 100644 index 1cb8dae..0000000 --- a/libs/halley/test/specs/websocket-server-restart-spec.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -var sinon = require('sinon'); - -module.exports = function() { - describe('websocket-server-restart', function() { - - it('should terminate if the server disconnects', function() { - var self = this; - var mock = sinon.mock(this.dispatcher); - mock.expects("handleError").once(); - - return this.websocket.connect() - .bind(this) - .then(function() { - return self.serverControl.restart(); - }) - .delay(10) - .then(function() { - mock.verify(); - }); - }); - - }); -}; diff --git a/libs/halley/test/specs/websocket-spec.js b/libs/halley/test/specs/websocket-spec.js deleted file mode 100644 index 9a31377..0000000 --- a/libs/halley/test/specs/websocket-spec.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -var sinon = require('sinon'); -var Promise = require('bluebird'); -var assert = require('assert'); - -module.exports = function() { - describe('websocket-transport', function() { - - it('should connect', function() { - return this.websocket.connect(); - }); - - it('should cancel connect', function() { - var connect = this.websocket.connect(); - connect.cancel(); - return Promise.delay(100) - .bind(this) - .then(function() { - assert(connect.isCancelled()); - assert(!this.websocket._socket); - assert.strictEqual(this.websocket._connectPromise, null); - }); - }); - - it('should notify on close', function() { - var mock = sinon.mock(this.dispatcher); - mock.expects("handleError").once(); - - return this.websocket.connect() - .bind(this) - .then(function() { - this.websocket.close(); - mock.verify(); - }); - }); - - }); -}; diff --git a/libs/halley/test/statemachine-mixin-test.js b/libs/halley/test/statemachine-mixin-test.js deleted file mode 100644 index d449df3..0000000 --- a/libs/halley/test/statemachine-mixin-test.js +++ /dev/null @@ -1,559 +0,0 @@ -'use strict'; - -var StateMachineMixin = require('../lib/mixins/statemachine-mixin'); -var assert = require('assert'); -var extend = require('../lib/util/externals').extend; -var Promise = require('bluebird'); - -describe('statemachine-mixin', function() { - - describe('normal flow', function() { - - beforeEach(function() { - - var TEST_FSM = { - name: "test", - initial: "A", - transitions: { - A: { - t1: "B" - }, - B: { - t2: "C" - }, - C: { - t3: "A" - } - } - }; - - function TestMachine() { - this.initStateMachine(TEST_FSM); - } - - TestMachine.prototype = { - }; - extend(TestMachine.prototype, StateMachineMixin); - - this.testMachine = new TestMachine(); - }); - - it('should transition', function() { - return this.testMachine.transitionState('t1') - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('B')); - }); - }); - - it('should serialize transitions', function() { - return Promise.all([ - this.testMachine.transitionState('t1'), - this.testMachine.transitionState('t2') - ]) - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('C')); - }); - }); - - it('should handle optional transitions', function() { - return this.testMachine.transitionState('doesnotexist', { optional: true }) - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('A')); - }); - }); - - it('should reject invalid transitions', function() { - return this.testMachine.transitionState('doesnotexist') - .bind(this) - .then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err.message, 'Unable to perform transition doesnotexist from state A'); - }); - }); - - it('should proceed with queued transitions after a transition has failed', function() { - return Promise.all([ - this.testMachine.transitionState('doesnotexist'), - this.testMachine.transitionState('t1'), - ].map(function(p) { return p.reflect(); })) - .bind(this) - .spread(function(p1, p2) { - assert(p1.isRejected()); - assert(p2.isFulfilled()); - assert(this.testMachine.stateIs('B')); - }); - - }); - }); - - describe('automatic transitioning', function() { - - beforeEach(function() { - - var TEST_FSM = { - name: "test", - initial: "A", - transitions: { - A: { - t1: "B", - t2: "C" - }, - B: { - t3: "C" - }, - C: { - t4: "A" - } - } - }; - - function TestMachine() { - this.initStateMachine(TEST_FSM); - } - - TestMachine.prototype = { - _onEnterA: function() { - return 't1'; - }, - _onEnterB: function() { - return 't3'; - }, - _onEnterC: function() { - } - }; - extend(TestMachine.prototype, StateMachineMixin); - - this.testMachine = new TestMachine(); - - }); - - it('should transition', function() { - return this.testMachine.transitionState('t1') - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('C')); - }); - }); - - it.skip('should reject on state transitions', function() { - return Promise.all([ - this.testMachine.waitForState({ - rejected: 'B', - fulfilled: 'C' - }), - this.testMachine.transitionState('t1') - ]) - .bind(this) - .then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err.message, 'State is B'); - }); - }); - - it.skip('should wait for state transitions when already in the state', function() { - return this.testMachine.waitForState({ - fulfilled: 'A' - }) - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('A')); - }); - }); - - it.skip('should reject state transitions when already in the state', function() { - return this.testMachine.waitForState({ - fulfilled: 'C', - rejected: 'A' - }) - .bind(this) - .then(function() { - assert.ok(false); - }, function() { - assert.ok(true); - }); - }); - - it.skip('should timeout waiting for state transitions', function() { - return this.testMachine.waitForState({ - fulfilled: 'C', - rejected: 'B', - timeout: 1 - }) - .bind(this) - .then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err.message, 'Timeout waiting for state C'); - }); - }); - - }); - - describe('error handling', function() { - - beforeEach(function() { - - var TEST_FSM = { - name: "test", - initial: "A", - transitions: { - A: { - t1: "B", - t4: "C", - t6: 'FAIL_ON_ENTER' - }, - B: { - t2: "C", - error: 'D' - }, - C: { - t5: 'E' - }, - D: { - t3: "E" - }, - E: { - - }, - FAIL_ON_ENTER: { - - } - } - }; - - function TestMachine() { - this.initStateMachine(TEST_FSM); - } - - TestMachine.prototype = { - _onLeaveC: function() { - }, - _onEnterB: function() { - throw new Error('Failed to enter B'); - }, - _onEnterFAIL_ON_ENTER: function() { - throw new Error('Failed on enter'); - } - }; - extend(TestMachine.prototype, StateMachineMixin); - - this.testMachine = new TestMachine(); - - }); - - it('should handle errors on transition', function() { - return this.testMachine.transitionState('t1') - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('D')); - }); - }); - - it('should transition to error state before other queued transitions', function() { - return Promise.all([ - this.testMachine.transitionState('t1'), - this.testMachine.transitionState('t3'), - ].map(function(p) { return p.reflect(); })) - .bind(this) - .spread(function(p1, p2) { - assert(p1.isFulfilled()); - assert(p2.isFulfilled()); - assert(this.testMachine.stateIs('E')); - }); - }); - - - it('should throw the original error if the state does not have an error transition', function() { - return this.testMachine.transitionState('t6') - .bind(this) - .then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err.message, 'Failed on enter'); - }); - }); - - }); - - describe('dedup', function() { - - beforeEach(function() { - - var TEST_FSM = { - name: "test", - initial: "A", - transitions: { - A: { - t1: "B" - }, - B: { - t1: "C" - }, - C: { - } - } - }; - - function TestMachine() { - this.initStateMachine(TEST_FSM); - } - - TestMachine.prototype = { - _onEnterB: function() { - this.bCount = this.bCount ? this.bCount + 1 : 1; - return Promise.delay(1); - }, - _onEnterC: function() { - return Promise.delay(1); - } - }; - extend(TestMachine.prototype, StateMachineMixin); - - this.testMachine = new TestMachine(); - }); - - it('should transition with dedup', function() { - return Promise.all([ - this.testMachine.transitionState('t1'), - this.testMachine.transitionState('t1', { dedup: true }), - ]) - .bind(this) - .then(function() { - assert.strictEqual(this.testMachine.bCount, 1); - assert(this.testMachine.stateIs('B')); - }); - }); - - it('should clearup pending transitions', function() { - return this.testMachine.transitionState('t1') - .bind(this) - .then(function() { - assert.strictEqual(this.testMachine.bCount, 1); - assert(this.testMachine.stateIs('B')); - assert.deepEqual(this.testMachine._pendingTransitions, {}); - }); - }); - - it('should transition with dedup followed by non-dedup', function() { - return Promise.all([ - this.testMachine.transitionState('t1'), - this.testMachine.transitionState('t1', { dedup: true }), - this.testMachine.transitionState('t1'), - ]) - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('C')); - assert.deepEqual(this.testMachine._pendingTransitions, {}); - }); - }); - - it('should dedup against the first pending transition', function() { - var p1 = this.testMachine.transitionState('t1'); - var p2 = this.testMachine.transitionState('t1'); - var p3 = this.testMachine.transitionState('t1', { dedup: true }) - .bind(this) - .then(function() { - assert(p1.isFulfilled()); - assert(p2.isPending()); - }); - - return Promise.all([p1, p2, p3]) - .bind(this) - .then(function() { - assert(this.testMachine.stateIs('C')); - assert.deepEqual(this.testMachine._pendingTransitions, {}); - }); - }); - - }); - - describe('cancellation', function() { - beforeEach(function() { - - var TEST_FSM = { - name: "test", - initial: "A", - transitions: { - A: { - t1: "B", - }, - B: { - t2: "C", - }, - C: { - t3: "D" - }, - D: { - } - } - }; - - function TestMachine() { - this.count = 0; - this.initStateMachine(TEST_FSM); - } - - TestMachine.prototype = { - _onEnterB: function() { - return Promise.delay(1); - }, - _onEnterC: function() { - return Promise.delay(5) - .bind(this) - .then(function() { - this.count++; - }); - } - }; - extend(TestMachine.prototype, StateMachineMixin); - - this.testMachine = new TestMachine(); - }); - - it('should handle cancellations before they execute', function() { - var p1 = this.testMachine.transitionState('t1'); - var p2 = this.testMachine.transitionState('t2'); - - p2.cancel(); - return Promise.delay(5) - .bind(this) - .then(function() { - assert(!p1.isCancelled()); - return p1; - }) - .then(function() { - assert(this.testMachine.stateIs('B')); - }); - }); - - it('should not cancel transitions after they start', function() { - var p; - return this.testMachine.transitionState('t1') - .bind(this) - .then(function() { - p = this.testMachine.transitionState('t2'); - return Promise.delay(1); - }) - .then(function() { - assert(p.isCancellable()); - p.cancel(); - return Promise.delay(5); - }) - .then(function() { - assert.strictEqual(this.testMachine.count, 1); - assert(this.testMachine.stateIs('C')); - }); - }); - - }); - - - describe('resetTransition', function() { - beforeEach(function() { - var self = this; - var TEST_FSM = { - name: "test", - initial: "A", - globalTransitions: { - disable: "DISABLED" - }, - transitions: { - A: { - t1: "B", - t2: "C", - t5: "D" - }, - B: { - t3: "C" - }, - C: { - t4: "B" - }, - D: { - t6: "A" - }, - DISABLED: { - - } - } - }; - - function TestMachine() { - this.initStateMachine(TEST_FSM); - } - - TestMachine.prototype = { - _onLeaveA: function() { - self.leaveACount++; - }, - _onEnterB: function() { - self.enterBCount++; - return Promise.delay(1, 't3'); - }, - _onLeaveB: function() { - self.leaveBCount++; - }, - _onEnterC: function() { - self.enterCCount++; - return Promise.delay(1, 't4'); - } - }; - extend(TestMachine.prototype, StateMachineMixin); - - this.testMachine = new TestMachine(); - this.leaveACount = 0; - this.enterBCount = 0; - this.leaveBCount = 0; - this.enterCCount = 0; - }); - - it('should transition', function() { - var promise = this.testMachine.transitionState('t1'); - - promise.catch(function() {}); // Prevent warnings here - - var resetReason = new Error('We need to reset'); - return this.testMachine.resetTransition('disable', resetReason) - .bind(this) - .then(function() { - assert.strictEqual(this.leaveACount, 1); - assert.strictEqual(this.enterBCount, 1); - assert.strictEqual(this.leaveBCount, 1); - assert.strictEqual(this.enterCCount, 0); - - assert(this.testMachine.stateIs('DISABLED')); - - return promise; // Ensure the original transition completed - }) - .then(function() { - assert.ok(false); - }, function(err) { - assert.strictEqual(err, resetReason); - }); - }); - - it('should not cancel the first transition', function() { - var promise1 = this.testMachine.transitionState('t5'); - var promise2 = this.testMachine.transitionState('t5'); - var promise3 = this.testMachine.transitionState('tXXX'); - - var resetReason = new Error('We need to reset'); - return this.testMachine.resetTransition('disable', resetReason) - .bind(this) - .then(function() { - assert.strictEqual(this.leaveACount, 1); - assert(this.testMachine.stateIs('DISABLED')); - assert(promise1.isFulfilled()); - assert(promise2.reason(), resetReason); - assert(promise3.reason(), resetReason); - }); - }); - - - }); -}); diff --git a/libs/halley/test/test-suite-browser.js b/libs/halley/test/test-suite-browser.js deleted file mode 100644 index 57d1c7e..0000000 --- a/libs/halley/test/test-suite-browser.js +++ /dev/null @@ -1,63 +0,0 @@ -/* jshint browser:true */ -'use strict'; - -require('../lib/util/externals').use({ - Events: require('backbone-events-standalone'), - extend: require('lodash/object/extend') -}); - -var Promise = require('bluebird'); -Promise.config({ - warnings: true, - longStackTraces: !!window.localStorage.BLUEBIRD_LONG_STACK_TRACES, - cancellation: true -}); - -var RemoteServerControl = require('./helpers/remote-server-control'); - -describe('browser integration tests', function() { - this.timeout(30000); - - before(function() { - this.serverControl = new RemoteServerControl(); - return this.serverControl.setup() - .bind(this) - .then(function(urls) { - this.urls = urls; - }); - }); - - after(function() { - return this.serverControl.teardown(); - }); - - beforeEach(function() { - this.urlDirect = this.urls.bayeux; - this.urlProxied = this.urls.proxied; - this.urlInvalid = 'https://127.0.0.2:65534/bayeux'; - - this.clientOptions = { - retry: 500, - timeout: 1000 - }; - }); - - afterEach(function() { - return this.serverControl.restoreAll(); - }); - - - require('./browser-websocket-test'); - require('./client-long-polling-test'); - require('./client-websockets-test'); - require('./client-all-transports-test'); -}); - -describe('browser unit tests', function() { - require('./errors-test'); - require('./promise-util-test'); - require('./channel-set-test'); - require('./extensions-test'); - require('./transport-pool-test'); - require('./statemachine-mixin-test'); -}); diff --git a/libs/halley/test/test-suite-node.js b/libs/halley/test/test-suite-node.js deleted file mode 100644 index 6edab43..0000000 --- a/libs/halley/test/test-suite-node.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -require('../lib/util/externals').use({ - Events: require('backbone-events-standalone'), - extend: require('lodash/object/extend') -}); - -var Promise = require('bluebird'); -Promise.config({ - warnings: true, - longStackTraces: true, - cancellation: true -}); - -var BayeuxWithProxyServer = require('./helpers/bayeux-with-proxy-server'); - -describe('node-test-suite', function() { - - before(function(done) { - var self = this; - this.server = new BayeuxWithProxyServer('localhost'); - - this.serverControl = this.server; - - this.server.start(function(err, urls) { - if (err) return done(err); - self.urls = urls; - done(); - }); - }); - - after(function(done) { - this.server.stop(done); - }); - - describe('integration tests', function() { - this.timeout(20000); - - before(function() { - /* Give server time to startup */ - this.timeout(20000); - return this.serverControl.restoreAll(); - }); - - beforeEach(function() { - this.urlDirect = this.urls.bayeux; - this.urlProxied = this.urls.proxied; - this.urlInvalid = 'https://127.0.0.2:65534/bayeux'; - - this.clientOptions = { - retry: 500, - timeout: 500 - }; - }); - - afterEach(function() { - return this.serverControl.restoreAll(); - }); - - require('./node-websocket-test'); - require('./client-long-polling-test'); - require('./client-websockets-test'); - require('./client-all-transports-test'); - require('./client-shutdown-test'); - }); - - - describe('unit tests', function() { - require('./errors-test'); - require('./promise-util-test'); - require('./channel-set-test'); - require('./extensions-test'); - require('./transport-pool-test'); - require('./statemachine-mixin-test'); - }); - - -}); diff --git a/libs/halley/test/transport-pool-test.js b/libs/halley/test/transport-pool-test.js deleted file mode 100644 index 7e87d5a..0000000 --- a/libs/halley/test/transport-pool-test.js +++ /dev/null @@ -1,242 +0,0 @@ -'use strict'; - -var TransportPool = require('../lib/transport/pool'); -var assert = require('assert'); -var Promise = require('bluebird'); -var sinon = require('sinon'); - -describe('transport pool', function() { - - beforeEach(function() { - this.PollingTransport = function() { }; - this.PollingTransport.prototype = { - connectionType: 'polling', - close: function() { - - }, - }; - - this.StreamingTransport = function() { }; - - this.StreamingTransport.prototype = { - connect: function() { - return Promise.delay(10); - }, - close: function() { - }, - connectionType: 'streaming' - }; - - this.StreamingFailTransport = function() { }; - this.StreamingFailTransport.prototype = { - connect: function() { - return Promise.delay(2) - .then(function() { - throw new Error('Connect fail'); - }); - }, - close: function() { - }, - connectionType: 'streaming-fail' - }; - - this.PollingTransport.isUsable = - this.StreamingTransport.isUsable = - this.StreamingFailTransport.isUsable = function() { - return true; - }; - - - this.dispatcher = { - handleResponse: function() { - }, - handleError: function() { - } - }; - - this.advice = { - reconnect: 'retry', - interval: 0, - timeout: 1000, - retry: 1 - }; - - this.endpoint = { href: 'http://localhost/bayeux' }; - - this.disabled = []; - - this.registeredTransports = [ - ['streaming-fail', this.StreamingFailTransport], - ['streaming', this.StreamingTransport], - ['polling', this.PollingTransport], - ]; - - this.transportPool = new TransportPool(this.dispatcher, this.endpoint, this.advice, this.disabled, this.registeredTransports); - }); - - describe('get', function() { - it('should return an instance of a transport', function() { - return this.transportPool.get() - .bind(this) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - }); - }); - - it('should return an instance of a polling transport', function() { - this.transportPool.setAllowed(['polling']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - }); - }); - - it('should return an instance of a streaming transport', function() { - this.transportPool.setAllowed(['streaming']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - assert(transport instanceof this.StreamingTransport); - }); - }); - - it('should fail if an async transport is unavailable', function() { - this.transportPool.setAllowed(['streaming-fail']); - return this.transportPool.get() - .bind(this) - .then(function() { - assert.ok(false, 'Expected a failure'); - }, function(e) { - assert.strictEqual(e.message, 'Connect fail'); - }); - }); - - it('should return polling transport and then switch to streaming when it comes online', function() { - this.transportPool.setAllowed(['streaming', 'polling']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - }) - .delay(10) - .then(function() { - return this.transportPool.reevaluate(); - }) - .then(function(transport) { - assert(transport instanceof this.StreamingTransport); - }); - }); - - it('should return polling transport and then switch to streaming when it comes online, even when some transport fail', function() { - this.transportPool.setAllowed(['streaming-fail', 'streaming', 'polling']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - }) - .delay(10) - .then(function() { - return this.transportPool.reevaluate(); - }) - .then(function(transport) { - assert(transport instanceof this.StreamingTransport); - }); - }); - - it('should close transports on close', function() { - this.transportPool.setAllowed(['streaming-fail', 'streaming', 'polling']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - var mock = sinon.mock(transport); - mock.expects("close").once(); - - this.transportPool.close(); - assert.deepEqual(this.transportPool._transports, {}); - mock.verify(); - }); - }); - - it('should reselect on down', function() { - var firstTransport; - - this.transportPool.setAllowed(['streaming', 'streaming-fail']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - var mock = sinon.mock(transport); - mock.expects("close").never(); - - firstTransport = transport; - assert(transport instanceof this.StreamingTransport); - this.transportPool.down(transport); - - mock.verify(); - - return this.transportPool.get(); - }) - .then(function(transport) { - assert(transport instanceof this.StreamingTransport); - assert.notStrictEqual(transport, firstTransport); - }); - }); - - it('should reselect on down with async and sync connections', function() { - this.transportPool.setAllowed(['streaming', 'polling']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - }) - .delay(10) - .then(function() { - return this.transportPool.reevaluate(); - }) - .then(function(transport) { - assert(transport instanceof this.StreamingTransport); - this.transportPool.down(transport); - return this.transportPool.get(); - }) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - }) - .delay(10) - .then(function() { - return this.transportPool.reevaluate(); - }) - .then(function() { - return this.transportPool.get(); - }) - .then(function(transport) { - assert(transport instanceof this.StreamingTransport); - }); - }); - - it('should handle multiple transports going down', function() { - var polling; - this.transportPool.setAllowed(['streaming', 'polling']); - return this.transportPool.get() - .bind(this) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - polling = transport; - }) - .delay(10) - .then(function() { - return this.transportPool.reevaluate(); - }) - .then(function(transport) { - assert(transport instanceof this.StreamingTransport); - this.transportPool.down(transport); - this.transportPool.down(polling); - return this.transportPool.get(); - }) - .then(function(transport) { - assert(transport instanceof this.PollingTransport); - assert.notStrictEqual(transport, polling); - }); - }); - - }); -}); From 969d8399ae6b1607afcefadc278eebc1f857e32a Mon Sep 17 00:00:00 2001 From: Terry Sahaidak Date: Wed, 21 Jun 2017 01:37:01 +0300 Subject: [PATCH 5/5] Fix icon sizes --- app/utils/iconsMap.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/utils/iconsMap.js b/app/utils/iconsMap.js index d343ced..0d39163 100644 --- a/app/utils/iconsMap.js +++ b/app/utils/iconsMap.js @@ -1,6 +1,5 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons' import {icons} from '../constants' -import {PixelRatio, Platform} from 'react-native' export const iconsMap = {}; export const iconsLoaded = new Promise((resolve, reject) => { @@ -10,7 +9,7 @@ export const iconsLoaded = new Promise((resolve, reject) => { const Provider = MaterialIcons return Provider.getImageSource( icon.icon, - Platform.OS === 'ios' ? icon.size : PixelRatio.getPixelSizeForLayoutSize(icon.size), + icon.size, icon.color ) })