Lidarr/frontend/src/Components/SignalRConnector.js

399 lines
9.9 KiB
JavaScript
Raw Normal View History

2017-09-04 02:20:56 +00:00
import $ from 'jquery';
import 'signalr';
2017-09-04 02:20:56 +00:00
import PropTypes from 'prop-types';
import { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
2017-10-07 06:47:28 +00:00
import { repopulatePage } from 'Utilities/pagePopulator';
import titleCase from 'Utilities/String/titleCase';
import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
2017-09-04 02:20:56 +00:00
import { setAppValue, setVersion } from 'Store/Actions/appActions';
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
import { fetchArtist } from 'Store/Actions/artistActions';
2017-09-04 02:20:56 +00:00
import { fetchHealth } from 'Store/Actions/systemActions';
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
2017-09-04 02:20:56 +00:00
function getState(status) {
switch (status) {
case 0:
return 'connecting';
case 1:
return 'connected';
case 2:
return 'reconnecting';
case 4:
return 'disconnected';
default:
throw new Error(`invalid status ${status}`);
}
}
function isAppDisconnected(disconnectedTime) {
if (!disconnectedTime) {
return false;
}
return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180;
}
function getHandlerName(name) {
name = titleCase(name);
name = name.replace('/', '');
return `handle${name}`;
}
2017-09-04 02:20:56 +00:00
function createMapStateToProps() {
return createSelector(
(state) => state.app.isReconnecting,
2017-10-07 06:47:28 +00:00
(state) => state.app.isDisconnected,
2017-09-04 02:20:56 +00:00
(state) => state.queue.paged.isPopulated,
2017-10-07 06:47:28 +00:00
(isReconnecting, isDisconnected, isQueuePopulated) => {
2017-09-04 02:20:56 +00:00
return {
isReconnecting,
2017-10-07 06:47:28 +00:00
isDisconnected,
2017-09-04 02:20:56 +00:00
isQueuePopulated
};
}
);
}
const mapDispatchToProps = {
dispatchFetchCommands: fetchCommands,
dispatchUpdateCommand: updateCommand,
dispatchFinishCommand: finishCommand,
dispatchSetAppValue: setAppValue,
dispatchSetVersion: setVersion,
dispatchUpdate: update,
dispatchUpdateItem: updateItem,
dispatchRemoveItem: removeItem,
dispatchFetchArtist: fetchArtist,
dispatchFetchHealth: fetchHealth,
dispatchFetchQueue: fetchQueue,
dispatchFetchQueueDetails: fetchQueueDetails,
dispatchFetchRootFolders: fetchRootFolders,
dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails
2017-09-04 02:20:56 +00:00
};
class SignalRConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.signalRconnectionOptions = { transport: ['webSockets', 'serverSentEvents', 'longPolling'] };
2017-09-04 02:20:56 +00:00
this.signalRconnection = null;
2017-11-26 20:09:45 +00:00
this.retryInterval = 1;
2017-09-04 02:20:56 +00:00
this.retryTimeoutId = null;
this.disconnectedTime = null;
2017-09-04 02:20:56 +00:00
}
componentDidMount() {
console.log('Starting signalR');
2017-09-04 02:20:56 +00:00
const url = `${window.Lidarr.urlBase}/signalr`;
this.signalRconnection = $.connection(url, { apiKey: window.Lidarr.apiKey });
2017-09-04 02:20:56 +00:00
this.signalRconnection.stateChanged(this.onStateChanged);
this.signalRconnection.received(this.onReceived);
this.signalRconnection.reconnecting(this.onReconnecting);
this.signalRconnection.disconnected(this.onDisconnected);
this.signalRconnection.start(this.signalRconnectionOptions);
}
componentWillUnmount() {
if (this.retryTimeoutId) {
this.retryTimeoutId = clearTimeout(this.retryTimeoutId);
}
2017-09-04 02:20:56 +00:00
this.signalRconnection.stop();
this.signalRconnection = null;
}
//
// Control
retryConnection = () => {
if (isAppDisconnected(this.disconnectedTime)) {
2017-10-28 20:13:32 +00:00
this.setState({
isDisconnected: true
});
}
2017-09-04 02:20:56 +00:00
this.retryTimeoutId = setTimeout(() => {
if (!this.signalRconnection) {
console.error('signalR: Connection was disposed');
return;
}
2017-09-04 02:20:56 +00:00
this.signalRconnection.start(this.signalRconnectionOptions);
2017-11-26 20:09:45 +00:00
this.retryInterval = Math.min(this.retryInterval + 1, 10);
2017-09-04 02:20:56 +00:00
}, this.retryInterval * 1000);
}
handleMessage = (message) => {
const {
name,
body
} = message;
const handler = this[getHandlerName(name)];
2017-09-04 02:20:56 +00:00
if (handler) {
handler(body);
return;
}
console.error(`signalR: Unable to find handler for ${name}`);
2017-09-04 02:20:56 +00:00
}
handleCalendar = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
2017-09-04 02:20:56 +00:00
section: 'calendar',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2017-09-04 02:20:56 +00:00
}
}
handleCommand = (body) => {
if (body.action === 'sync') {
this.props.dispatchFetchCommands();
return;
}
2017-09-04 02:20:56 +00:00
const resource = body.resource;
const status = resource.status;
2017-09-04 02:20:56 +00:00
// Both sucessful and failed commands need to be
// completed, otherwise they spin until they timeout.
if (status === 'completed' || status === 'failed') {
this.props.dispatchFinishCommand(resource);
2017-09-04 02:20:56 +00:00
} else {
this.props.dispatchUpdateCommand(resource);
2017-09-04 02:20:56 +00:00
}
}
handleAlbum = (body) => {
2017-09-04 02:20:56 +00:00
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'albums',
2017-09-04 02:20:56 +00:00
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2017-09-04 02:20:56 +00:00
}
}
handleTrack = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'tracks',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
}
}
handleTrackfile = (body) => {
const section = 'trackFiles';
2017-09-04 02:20:56 +00:00
if (body.action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource });
} else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
2017-09-04 02:20:56 +00:00
}
// Repopulate the page to handle recently imported file
repopulatePage('trackFileUpdated');
2017-09-04 02:20:56 +00:00
}
handleHealth = () => {
this.props.dispatchFetchHealth();
2017-09-04 02:20:56 +00:00
}
2017-10-07 22:38:31 +00:00
handleArtist = (body) => {
2017-09-04 02:20:56 +00:00
const action = body.action;
2017-10-07 22:38:31 +00:00
const section = 'artist';
2017-09-04 02:20:56 +00:00
if (action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource });
2017-09-04 02:20:56 +00:00
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
2017-09-04 02:20:56 +00:00
}
}
handleQueue = () => {
2017-09-04 02:20:56 +00:00
if (this.props.isQueuePopulated) {
this.props.dispatchFetchQueue();
2017-09-04 02:20:56 +00:00
}
}
handleQueueDetails = () => {
this.props.dispatchFetchQueueDetails();
2017-09-04 02:20:56 +00:00
}
handleQueueStatus = (body) => {
this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
2017-09-04 02:20:56 +00:00
}
handleVersion = (body) => {
const version = body.Version;
2017-09-04 02:20:56 +00:00
this.props.dispatchSetVersion({ version });
2017-09-04 02:20:56 +00:00
}
handleWantedCutoff = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
2017-09-04 02:20:56 +00:00
section: 'cutoffUnmet',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2017-09-04 02:20:56 +00:00
}
}
handleWantedMissing = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
2017-09-04 02:20:56 +00:00
section: 'missing',
updateOnly: true,
2017-10-07 06:47:28 +00:00
...body.resource
});
2017-09-04 02:20:56 +00:00
}
}
handleSystemTask = () => {
// No-op for now, we may want this later
}
handleRootfolder = () => {
this.props.dispatchFetchRootFolders();
}
handleTag = (body) => {
if (body.action === 'sync') {
this.props.dispatchFetchTags();
this.props.dispatchFetchTagDetails();
return;
}
}
2017-09-04 02:20:56 +00:00
//
// Listeners
onStateChanged = (change) => {
const state = getState(change.newState);
console.log(`signalR: ${state}`);
2017-09-04 02:20:56 +00:00
if (state === 'connected') {
// Clear disconnected time
this.disconnectedTime = null;
const {
dispatchFetchCommands,
dispatchFetchArtist,
dispatchSetAppValue
} = this.props;
2017-10-07 06:47:28 +00:00
// Repopulate the page (if a repopulator is set) to ensure things
// are in sync after reconnecting.
if (this.props.isReconnecting || this.props.isDisconnected) {
dispatchFetchArtist();
dispatchFetchCommands();
2017-10-07 06:47:28 +00:00
repopulatePage();
}
dispatchSetAppValue({
2017-09-04 02:20:56 +00:00
isConnected: true,
isReconnecting: false,
isDisconnected: false,
isRestarting: false
2017-09-04 02:20:56 +00:00
});
this.retryInterval = 5;
if (this.retryTimeoutId) {
clearTimeout(this.retryTimeoutId);
}
}
}
onReceived = (message) => {
console.debug('signalR: received', message.name, message.body);
2017-09-04 02:20:56 +00:00
this.handleMessage(message);
}
onReconnecting = () => {
if (window.Lidarr.unloading) {
2017-09-04 02:20:56 +00:00
return;
}
if (!this.disconnectedTime) {
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
}
this.props.dispatchSetAppValue({
2017-09-04 02:20:56 +00:00
isReconnecting: true
});
}
onDisconnected = () => {
if (window.Lidarr.unloading) {
2017-10-02 03:05:28 +00:00
return;
2017-09-04 02:20:56 +00:00
}
2017-10-02 03:05:28 +00:00
if (!this.disconnectedTime) {
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
}
this.props.dispatchSetAppValue({
2017-10-02 03:05:28 +00:00
isConnected: false,
isReconnecting: true,
isDisconnected: isAppDisconnected(this.disconnectedTime)
2017-10-02 03:05:28 +00:00
});
this.retryConnection();
2017-09-04 02:20:56 +00:00
}
//
// Render
render() {
return null;
}
}
SignalRConnector.propTypes = {
isReconnecting: PropTypes.bool.isRequired,
2017-10-07 06:47:28 +00:00
isDisconnected: PropTypes.bool.isRequired,
2017-09-04 02:20:56 +00:00
isQueuePopulated: PropTypes.bool.isRequired,
dispatchFetchCommands: PropTypes.func.isRequired,
dispatchUpdateCommand: PropTypes.func.isRequired,
dispatchFinishCommand: PropTypes.func.isRequired,
dispatchSetAppValue: PropTypes.func.isRequired,
dispatchSetVersion: PropTypes.func.isRequired,
dispatchUpdate: PropTypes.func.isRequired,
dispatchUpdateItem: PropTypes.func.isRequired,
dispatchRemoveItem: PropTypes.func.isRequired,
dispatchFetchArtist: PropTypes.func.isRequired,
dispatchFetchHealth: PropTypes.func.isRequired,
dispatchFetchQueue: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired
2017-09-04 02:20:56 +00:00
};
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);