From 16465f4239bf3bb05670be071d71a75cb7e2ecb4 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Wed, 7 Oct 2020 00:25:42 +0800 Subject: [PATCH] server: migrate method call configs to immutable arrays --- client/src/javascript/stores/TorrentStore.ts | 6 +- server/constants/fileListMethodCallConfigs.ts | 36 +++ server/constants/fileListPropMap.ts | 34 --- server/constants/rTorrentMethodCall.ts | 21 ++ .../constants/torrentListMethodCallConfigs.ts | 228 +++++++++++++++++ server/constants/torrentListPropMap.ts | 229 ------------------ .../transferSummaryMethodCallConfigs.ts | 36 +++ server/constants/transferSummaryPropMap.ts | 33 --- server/services/clientGatewayService.ts | 147 ++++++----- server/services/clientRequestManager.ts | 2 +- server/services/historyService.ts | 7 +- server/services/taxonomyService.ts | 4 +- server/services/torrentService.ts | 14 +- server/util/methodCallUtil.js | 28 --- server/util/torrentPropertiesUtil.ts | 24 +- shared/types/ServerEvents.ts | 4 +- shared/types/Torrent.ts | 7 +- 17 files changed, 428 insertions(+), 432 deletions(-) create mode 100644 server/constants/fileListMethodCallConfigs.ts delete mode 100644 server/constants/fileListPropMap.ts create mode 100644 server/constants/rTorrentMethodCall.ts create mode 100644 server/constants/torrentListMethodCallConfigs.ts delete mode 100644 server/constants/torrentListPropMap.ts create mode 100644 server/constants/transferSummaryMethodCallConfigs.ts delete mode 100644 server/constants/transferSummaryPropMap.ts delete mode 100644 server/util/methodCallUtil.js diff --git a/client/src/javascript/stores/TorrentStore.ts b/client/src/javascript/stores/TorrentStore.ts index 400e1e9d..f5686d52 100644 --- a/client/src/javascript/stores/TorrentStore.ts +++ b/client/src/javascript/stores/TorrentStore.ts @@ -1,4 +1,4 @@ -import type {TorrentDetails, TorrentProperties, TorrentListDiff, Torrents} from '@shared/types/Torrent'; +import type {TorrentDetails, TorrentProperties, TorrentListDiff, TorrentList} from '@shared/types/Torrent'; import AlertStore from './AlertStore'; import AppDispatcher from '../dispatcher/AppDispatcher'; @@ -23,7 +23,7 @@ class TorrentStoreClass extends BaseStore { selectedTorrents: Array = []; sortedTorrents: Array = []; sortTorrentsBy: FloodSettings['sortTorrents'] = SettingsStore.getFloodSetting('sortTorrents'); - torrents: Torrents = {}; + torrents: TorrentList = {}; fetchTorrentDetails(hash: TorrentProperties['hash'], forceUpdate?: boolean) { if (!this.isRequestPending('fetch-torrent-details') || forceUpdate) { @@ -244,7 +244,7 @@ class TorrentStoreClass extends BaseStore { this.emit('CLIENT_TORRENTS_REQUEST_SUCCESS'); } - handleTorrentListFullUpdate(torrentList: Torrents) { + handleTorrentListFullUpdate(torrentList: TorrentList) { this.torrents = torrentList; this.sortTorrents(); diff --git a/server/constants/fileListMethodCallConfigs.ts b/server/constants/fileListMethodCallConfigs.ts new file mode 100644 index 00000000..da956e42 --- /dev/null +++ b/server/constants/fileListMethodCallConfigs.ts @@ -0,0 +1,36 @@ +import {defaultTransformer, numberTransformer} from './rTorrentMethodCall'; + +const fileListMethodCallConfigs = [ + { + propLabel: 'path', + methodCall: 'f.path=', + transformValue: defaultTransformer, + }, + { + propLabel: 'pathComponents', + methodCall: 'f.path_components=', + transformValue: defaultTransformer, + }, + { + propLabel: 'priority', + methodCall: 'f.priority=', + transformValue: defaultTransformer, + }, + { + propLabel: 'sizeBytes', + methodCall: 'f.size_bytes=', + transformValue: numberTransformer, + }, + { + propLabel: 'sizeChunks', + methodCall: 'f.size_chunks=', + transformValue: numberTransformer, + }, + { + propLabel: 'completedChunks', + methodCall: 'f.completed_chunks=', + transformValue: numberTransformer, + }, +] as const; + +export default fileListMethodCallConfigs; diff --git a/server/constants/fileListPropMap.ts b/server/constants/fileListPropMap.ts deleted file mode 100644 index cfc6cc92..00000000 --- a/server/constants/fileListPropMap.ts +++ /dev/null @@ -1,34 +0,0 @@ -const fileListPropMap = new Map(); -const defaultTransformer = (value: unknown) => value; - -fileListPropMap.set('path', { - methodCall: 'f.path=', - transformValue: defaultTransformer, -}); - -fileListPropMap.set('pathComponents', { - methodCall: 'f.path_components=', - transformValue: defaultTransformer, -}); - -fileListPropMap.set('priority', { - methodCall: 'f.priority=', - transformValue: defaultTransformer, -}); - -fileListPropMap.set('sizeBytes', { - methodCall: 'f.size_bytes=', - transformValue: Number, -}); - -fileListPropMap.set('sizeChunks', { - methodCall: 'f.size_chunks=', - transformValue: Number, -}); - -fileListPropMap.set('completedChunks', { - methodCall: 'f.completed_chunks=', - transformValue: Number, -}); - -export default fileListPropMap; diff --git a/server/constants/rTorrentMethodCall.ts b/server/constants/rTorrentMethodCall.ts new file mode 100644 index 00000000..cb7515f5 --- /dev/null +++ b/server/constants/rTorrentMethodCall.ts @@ -0,0 +1,21 @@ +export interface MethodCallConfig { + readonly propLabel: string; + readonly methodCall: string; + readonly transformValue: (value: string) => string | string[] | boolean | number; +} + +export type MethodCallConfigs = Readonly>; + +export type MultiMethodCalls = Array<{methodName: string; params: Array}>; + +export const defaultTransformer = (value: string): string => { + return value; +}; + +export const booleanTransformer = (value: string): boolean => { + return value === '1'; +}; + +export const numberTransformer = (value: string): number => { + return Number(value); +}; diff --git a/server/constants/torrentListMethodCallConfigs.ts b/server/constants/torrentListMethodCallConfigs.ts new file mode 100644 index 00000000..6fa6d63c --- /dev/null +++ b/server/constants/torrentListMethodCallConfigs.ts @@ -0,0 +1,228 @@ +import {defaultTransformer, booleanTransformer, numberTransformer} from './rTorrentMethodCall'; +import regEx from '../../shared/util/regEx'; + +const torrentListMethodCallConfigs = [ + { + propLabel: 'hash', + methodCall: 'd.hash=', + transformValue: defaultTransformer, + }, + { + propLabel: 'name', + methodCall: 'd.name=', + transformValue: defaultTransformer, + }, + { + propLabel: 'message', + methodCall: 'd.message=', + transformValue: defaultTransformer, + }, + { + propLabel: 'state', + methodCall: 'd.state=', + transformValue: defaultTransformer, + }, + { + propLabel: 'isStateChanged', + methodCall: 'd.state_changed=', + transformValue: booleanTransformer, + }, + { + propLabel: 'isActive', + methodCall: 'd.is_active=', + transformValue: booleanTransformer, + }, + { + propLabel: 'isComplete', + methodCall: 'd.complete=', + transformValue: booleanTransformer, + }, + { + propLabel: 'isHashing', + methodCall: 'd.hashing=', + transformValue: defaultTransformer, + }, + { + propLabel: 'isOpen', + methodCall: 'd.is_open=', + transformValue: booleanTransformer, + }, + { + propLabel: 'priority', + methodCall: 'd.priority=', + transformValue: numberTransformer, + }, + { + propLabel: 'upRate', + methodCall: 'd.up.rate=', + transformValue: numberTransformer, + }, + { + propLabel: 'upTotal', + methodCall: 'd.up.total=', + transformValue: numberTransformer, + }, + { + propLabel: 'downRate', + methodCall: 'd.down.rate=', + transformValue: numberTransformer, + }, + { + propLabel: 'downTotal', + methodCall: 'd.down.total=', + transformValue: numberTransformer, + }, + { + propLabel: 'ratio', + methodCall: 'd.ratio=', + transformValue: numberTransformer, + }, + { + propLabel: 'bytesDone', + methodCall: 'd.bytes_done=', + transformValue: numberTransformer, + }, + { + propLabel: 'sizeBytes', + methodCall: 'd.size_bytes=', + transformValue: numberTransformer, + }, + { + propLabel: 'directory', + methodCall: 'd.directory=', + transformValue: defaultTransformer, + }, + { + propLabel: 'basePath', + methodCall: 'd.base_path=', + transformValue: defaultTransformer, + }, + { + propLabel: 'baseFilename', + methodCall: 'd.base_filename=', + transformValue: defaultTransformer, + }, + { + propLabel: 'baseDirectory', + methodCall: 'd.directory_base=', + transformValue: defaultTransformer, + }, + { + propLabel: 'seedingTime', + methodCall: 'd.custom=seedingtime', + transformValue: defaultTransformer, + }, + { + propLabel: 'dateAdded', + methodCall: 'd.custom=addtime', + transformValue: numberTransformer, + }, + { + propLabel: 'dateCreated', + methodCall: 'd.creation_date=', + transformValue: numberTransformer, + }, + { + propLabel: 'throttleName', + methodCall: 'd.throttle_name=', + transformValue: defaultTransformer, + }, + { + propLabel: 'isMultiFile', + methodCall: 'd.is_multi_file=', + transformValue: booleanTransformer, + }, + { + propLabel: 'isPrivate', + methodCall: 'd.is_private=', + transformValue: booleanTransformer, + }, + { + propLabel: 'tags', + methodCall: 'd.custom1=', + transformValue: (value: string) => { + if (value === '') { + return []; + } + + return value + .split(',') + .sort() + .map((tag) => decodeURIComponent(tag)); + }, + }, + { + propLabel: 'comment', + methodCall: 'd.custom2=', + transformValue: (value: string) => { + let comment = decodeURIComponent(value); + + if (comment.match(/^VRS24mrker/)) { + comment = comment.substr(10); + } + + return comment; + }, + }, + { + propLabel: 'trackerURIs', + methodCall: 'cat="$t.multicall=d.hash=,t.is_enabled=,t.url=,cat={|||}"', + transformValue: (value: string) => { + const trackers = value.split('|||'); + const trackerDomains: Array = []; + + trackers.forEach((tracker) => { + // Only count enabled trackers + if (tracker.charAt(0) === '0') { + return; + } + + const regexMatched = regEx.domainName.exec(tracker.substr(1)); + + if (regexMatched != null && regexMatched[1]) { + let domain = regexMatched[1]; + + const minSubsetLength = 3; + const domainSubsets = domain.split('.'); + let desiredSubsets = 2; + + if (domainSubsets.length > desiredSubsets) { + const lastDesiredSubset = domainSubsets[domainSubsets.length - desiredSubsets]; + if (lastDesiredSubset.length <= minSubsetLength) { + desiredSubsets += 1; + } + } + + domain = domainSubsets.slice(desiredSubsets * -1).join('.'); + + trackerDomains.push(domain); + } + }); + + // Deduplicate + return [...new Set(trackerDomains)]; + }, + }, + { + propLabel: 'seedsConnected', + methodCall: 'd.peers_complete=', + transformValue: numberTransformer, + }, + { + propLabel: 'seedsTotal', + methodCall: 'cat="$t.multicall=d.hash=,t.scrape_complete=,cat={|||}"', + transformValue: (value: string) => Number(value.substr(0, value.indexOf('|||'))), + }, + { + propLabel: 'peersConnected', + methodCall: 'd.peers_accounted=', + transformValue: numberTransformer, + }, + { + propLabel: 'peersTotal', + methodCall: 'cat="$t.multicall=d.hash=,t.scrape_incomplete=,cat={|||}"', + transformValue: (value: string) => Number(value.substr(0, value.indexOf('|||'))), + }, +] as const; + +export default torrentListMethodCallConfigs; diff --git a/server/constants/torrentListPropMap.ts b/server/constants/torrentListPropMap.ts deleted file mode 100644 index 41418deb..00000000 --- a/server/constants/torrentListPropMap.ts +++ /dev/null @@ -1,229 +0,0 @@ -import regEx from '../../shared/util/regEx'; - -const torrentListPropMap = new Map(); - -const booleanTransformer = (value: string) => value === '1'; -const defaultTransformer = (value: unknown) => value; - -torrentListPropMap.set('hash', { - methodCall: 'd.hash=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('name', { - methodCall: 'd.name=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('message', { - methodCall: 'd.message=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('state', { - methodCall: 'd.state=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('isStateChanged', { - methodCall: 'd.state_changed=', - transformValue: booleanTransformer, -}); - -torrentListPropMap.set('isActive', { - methodCall: 'd.is_active=', - transformValue: booleanTransformer, -}); - -torrentListPropMap.set('isComplete', { - methodCall: 'd.complete=', - transformValue: booleanTransformer, -}); - -torrentListPropMap.set('isHashing', { - methodCall: 'd.hashing=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('isOpen', { - methodCall: 'd.is_open=', - transformValue: booleanTransformer, -}); - -torrentListPropMap.set('priority', { - methodCall: 'd.priority=', - transformValue: Number, -}); - -torrentListPropMap.set('upRate', { - methodCall: 'd.up.rate=', - transformValue: Number, -}); - -torrentListPropMap.set('upTotal', { - methodCall: 'd.up.total=', - transformValue: Number, -}); - -torrentListPropMap.set('downRate', { - methodCall: 'd.down.rate=', - transformValue: Number, -}); - -torrentListPropMap.set('downTotal', { - methodCall: 'd.down.total=', - transformValue: Number, -}); - -torrentListPropMap.set('ratio', { - methodCall: 'd.ratio=', - transformValue: Number, -}); - -torrentListPropMap.set('bytesDone', { - methodCall: 'd.bytes_done=', - transformValue: Number, -}); - -torrentListPropMap.set('sizeBytes', { - methodCall: 'd.size_bytes=', - transformValue: Number, -}); - -torrentListPropMap.set('directory', { - methodCall: 'd.directory=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('basePath', { - methodCall: 'd.base_path=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('baseFilename', { - methodCall: 'd.base_filename=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('baseDirectory', { - methodCall: 'd.directory_base=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('seedingTime', { - methodCall: 'd.custom=seedingtime', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('dateAdded', { - methodCall: 'd.custom=addtime', - transformValue: Number, -}); - -torrentListPropMap.set('dateCreated', { - methodCall: 'd.creation_date=', - transformValue: Number, -}); - -torrentListPropMap.set('throttleName', { - methodCall: 'd.throttle_name=', - transformValue: defaultTransformer, -}); - -torrentListPropMap.set('isMultiFile', { - methodCall: 'd.is_multi_file=', - transformValue: booleanTransformer, -}); - -torrentListPropMap.set('isPrivate', { - methodCall: 'd.is_private=', - transformValue: booleanTransformer, -}); - -torrentListPropMap.set('tags', { - methodCall: 'd.custom1=', - transformValue: (value: string) => { - if (value === '') { - return []; - } - - return value - .split(',') - .sort() - .map((tag) => decodeURIComponent(tag)); - }, -}); - -torrentListPropMap.set('comment', { - methodCall: 'd.custom2=', - transformValue: (value: string) => { - let comment = decodeURIComponent(value); - - if (comment.match(/^VRS24mrker/)) { - comment = comment.substr(10); - } - - return comment; - }, -}); - -torrentListPropMap.set('trackerURIs', { - methodCall: 'cat="$t.multicall=d.hash=,t.is_enabled=,t.url=,cat={|||}"', - transformValue: (value: string) => { - const trackers = value.split('|||'); - const trackerDomains: Array = []; - - trackers.forEach((tracker) => { - // Only count enabled trackers - if (tracker.charAt(0) === '0') { - return; - } - - const regexMatched = regEx.domainName.exec(tracker.substr(1)); - - if (regexMatched != null && regexMatched[1]) { - let domain = regexMatched[1]; - - const minSubsetLength = 3; - const domainSubsets = domain.split('.'); - let desiredSubsets = 2; - - if (domainSubsets.length > desiredSubsets) { - const lastDesiredSubset = domainSubsets[domainSubsets.length - desiredSubsets]; - if (lastDesiredSubset.length <= minSubsetLength) { - desiredSubsets += 1; - } - } - - domain = domainSubsets.slice(desiredSubsets * -1).join('.'); - - trackerDomains.push(domain); - } - }); - - // Deduplicate - return [...new Set(trackerDomains)]; - }, -}); - -torrentListPropMap.set('seedsConnected', { - methodCall: 'd.peers_complete=', - transformValue: Number, -}); - -torrentListPropMap.set('seedsTotal', { - methodCall: 'cat="$t.multicall=d.hash=,t.scrape_complete=,cat={|||}"', - transformValue: (value: string) => Number(value.substr(0, value.indexOf('|||'))), -}); - -torrentListPropMap.set('peersConnected', { - methodCall: 'd.peers_accounted=', - transformValue: Number, -}); - -torrentListPropMap.set('peersTotal', { - methodCall: 'cat="$t.multicall=d.hash=,t.scrape_incomplete=,cat={|||}"', - transformValue: (value: string) => Number(value.substr(0, value.indexOf('|||'))), -}); - -export default torrentListPropMap; diff --git a/server/constants/transferSummaryMethodCallConfigs.ts b/server/constants/transferSummaryMethodCallConfigs.ts new file mode 100644 index 00000000..312592e6 --- /dev/null +++ b/server/constants/transferSummaryMethodCallConfigs.ts @@ -0,0 +1,36 @@ +import {numberTransformer} from './rTorrentMethodCall'; + +const transferSummaryMethodCallConfigs = [ + { + propLabel: 'upRate', + methodCall: 'throttle.global_up.rate', + transformValue: numberTransformer, + }, + { + propLabel: 'upTotal', + methodCall: 'throttle.global_up.total', + transformValue: numberTransformer, + }, + { + propLabel: 'upThrottle', + methodCall: 'throttle.global_up.max_rate', + transformValue: numberTransformer, + }, + { + propLabel: 'downRate', + methodCall: 'throttle.global_down.rate', + transformValue: numberTransformer, + }, + { + propLabel: 'downTotal', + methodCall: 'throttle.global_down.total', + transformValue: numberTransformer, + }, + { + propLabel: 'downThrottle', + methodCall: 'throttle.global_down.max_rate', + transformValue: numberTransformer, + }, +] as const; + +export default transferSummaryMethodCallConfigs; diff --git a/server/constants/transferSummaryPropMap.ts b/server/constants/transferSummaryPropMap.ts deleted file mode 100644 index 4ae8bc37..00000000 --- a/server/constants/transferSummaryPropMap.ts +++ /dev/null @@ -1,33 +0,0 @@ -const transferSummaryPropMap = new Map(); - -transferSummaryPropMap.set('upRate', { - methodCall: 'throttle.global_up.rate', - transformValue: Number, -}); - -transferSummaryPropMap.set('upTotal', { - methodCall: 'throttle.global_up.total', - transformValue: Number, -}); - -transferSummaryPropMap.set('upThrottle', { - methodCall: 'throttle.global_up.max_rate', - transformValue: Number, -}); - -transferSummaryPropMap.set('downRate', { - methodCall: 'throttle.global_down.rate', - transformValue: Number, -}); - -transferSummaryPropMap.set('downTotal', { - methodCall: 'throttle.global_down.total', - transformValue: Number, -}); - -transferSummaryPropMap.set('downThrottle', { - methodCall: 'throttle.global_down.max_rate', - transformValue: Number, -}); - -export default transferSummaryPropMap; diff --git a/server/services/clientGatewayService.ts b/server/services/clientGatewayService.ts index aba28d84..d87f58c0 100644 --- a/server/services/clientGatewayService.ts +++ b/server/services/clientGatewayService.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import {moveSync} from 'fs-extra'; import type {Credentials} from '@shared/types/Auth'; -import type {TorrentProperties, Torrents} from '@shared/types/Torrent'; +import type {TorrentList, TorrentListSummary, TorrentProperties} from '@shared/types/Torrent'; import type {TransferSummary} from '@shared/types/TransferData'; import type { CheckTorrentsOptions, @@ -15,34 +15,29 @@ import type { } from '@shared/types/Action'; import BaseService from './BaseService'; -import fileListPropMap from '../constants/fileListPropMap'; +import fileListMethodCallConfigs from '../constants/fileListMethodCallConfigs'; import fileUtil from '../util/fileUtil'; -import methodCallUtil from '../util/methodCallUtil'; import scgiUtil from '../util/scgiUtil'; -import type {MultiMethodCalls} from './clientRequestManager'; +import type {MethodCallConfigs, MultiMethodCalls} from '../constants/rTorrentMethodCall'; + +const filePathMethodCalls = fileListMethodCallConfigs + .filter((config) => config.propLabel === 'pathComponents') + .map((config) => config.methodCall); interface ClientGatewayServiceEvents { CLIENT_CONNECTION_STATE_CHANGE: () => void; PROCESS_TORRENT_LIST_START: () => void; - PROCESS_TORRENT_LIST_END: (processedTorrentList: {torrents: Torrents}) => void; + PROCESS_TORRENT_LIST_END: (processedTorrentList: {torrents: TorrentList}) => void; PROCESS_TORRENT: (processedTorrentDetailValues: TorrentProperties) => void; PROCESS_TRANSFER_RATE_START: () => void; } interface TorrentListReducer { key: T; - reduce: (properties: TorrentProperties) => TorrentProperties[T]; + reduce: (properties: Record) => TorrentProperties[T]; } -interface MethodCallConfig { - methodCalls: Array; - propLabels: Array; - valueTransformations: Array<(value: string) => string | number | boolean>; -} - -const fileListMethodCallConfig = methodCallUtil.getMethodCallConfigFromPropMap(fileListPropMap, ['pathComponents']); - class ClientGatewayService extends BaseService { hasError: boolean | null = null; torrentListReducers: Array = []; @@ -187,7 +182,7 @@ class ClientGatewayService extends BaseService { accumulator[index] = { methodName: 'f.multicall', - params: [hash, ''].concat(fileListMethodCallConfig.methodCalls), + params: [hash, ''].concat(filePathMethodCalls), }; accumulator[directoryBaseMethodCallIndex] = { @@ -327,38 +322,36 @@ class ClientGatewayService extends BaseService { /** * Sends a multicall request to rTorrent with the requested method calls. * - * @param {Object} options - An object of options... - * @param {Array} options.methodCalls - An array of strings representing - * method calls, which the client uses to retrieve details. - * @param {Array} options.propLabels - An array of strings that are used as - * keys for the transformed torrent details. - * @param {Array} options.valueTransformations - An array of functions that - * will be called with the values as returned by the client. These return - * values will be assigned to the key from the propLabels array. + * @param {MethodCallConfigs} configs - An array of method call config... * @return {Promise} - Resolves with the processed client response or rejects * with the processed client error. */ - async fetchTorrentList(options: MethodCallConfig) { + async fetchTorrentList(configs: MethodCallConfigs) { return ( this.services?.clientRequestManager - .methodCall('d.multicall2', ['', 'main'].concat(options.methodCalls)) + .methodCall('d.multicall2', ['', 'main'].concat(configs.map((config) => config.methodCall))) .then(this.processClientRequestSuccess) .then( - (torrents) => this.processTorrentListResponse(torrents as Array>, options), + (torrents) => this.processTorrentListResponse(torrents as Array>, configs), this.processClientRequestError, ) || Promise.reject() ); } - async fetchTransferSummary(options: MethodCallConfig) { - const methodCalls = options.methodCalls.map((methodName) => ({methodName, params: []})); + async fetchTransferSummary(configs: MethodCallConfigs) { + const methodCalls: MultiMethodCalls = configs.map((config) => { + return { + methodName: config.methodCall, + params: [], + }; + }); return ( this.services?.clientRequestManager .methodCall('system.multicall', [methodCalls]) .then(this.processClientRequestSuccess) .then( - (transferRate) => this.processTransferRateResponse(transferRate as Array, options), + (transferRate) => this.processTransferRateResponse(transferRate as Array, configs), this.processClientRequestError, ) || Promise.reject() ); @@ -385,76 +378,76 @@ class ClientGatewayService extends BaseService { * After rTorrent responds with the requested torrent details, we construct * an object with hashes as keys and processed details as values. * - * @param {Array} response - The array of all torrents and their details. - * @param {Object} options - An object of options that instruct us how to - * process the client's response. - * @param {Array} options.propLabels - An array of strings that map to the - * method call. These are the keys of the torrent details. - * @param {Array} options.valueTransformations - An array of functions that - * transform the detail from the client's response. + * @param {Array} response - The array of all torrents and their details. + * @param {MethodCallConfigs} configs - An array of method call config... * @return {Object} - An object that represents all torrents with hashes as * keys, each value being an object of detail labels and values. */ - processTorrentListResponse( + async processTorrentListResponse( torrentList: Array>, - {propLabels, valueTransformations}: MethodCallConfig, - ): {id: number; torrents: Torrents} { + configs: MethodCallConfigs, + ): Promise { this.emit('PROCESS_TORRENT_LIST_START'); // We map the array of details to objects with sensibly named keys. We want // to return an object with torrent hashes as keys and an object of torrent // details as values. - const processedTorrentList = torrentList.reduce( - (listAccumulator, torrentDetailValues) => { - // Transform the array of torrent detail values to an object with - // sensibly named keys. - let processedTorrentDetailValues = (torrentDetailValues.reduce( - (valueAccumulator: Record, value: string, valueIndex: number) => { - const key = propLabels[valueIndex]; - const transformValue = valueTransformations[valueIndex]; + const processedTorrentList = Object.assign( + {}, + ...(await Promise.all( + torrentList.map(async (torrentDetailValues) => { + // Transform the array of torrent detail values to an object with + // sensibly named keys. + const processingTorrentDetailValues = torrentDetailValues.reduce( + (accumulator: Record, value: string, index: number) => { + const {propLabel, transformValue} = configs[index]; - return Object.assign(valueAccumulator, {[key]: transformValue(value)}); - }, - {}, - ) as unknown) as TorrentProperties; + accumulator[propLabel] = transformValue(value); - // Assign values from external reducers to the torrent list object. - this.torrentListReducers.forEach((reducer) => { - const {key, reduce} = reducer; + return accumulator; + }, + {}, + ); - processedTorrentDetailValues = Object.assign(processedTorrentDetailValues, { - [key]: reduce(processedTorrentDetailValues), + // Assign values from external reducers to the torrent list object. + this.torrentListReducers.forEach((reducer) => { + const {key, reduce} = reducer; + processingTorrentDetailValues[key] = reduce(processingTorrentDetailValues); }); - }); - this.emit('PROCESS_TORRENT', processedTorrentDetailValues); + const processedTorrentDetailValues = (processingTorrentDetailValues as unknown) as TorrentProperties; - return { - id: listAccumulator.id, - torrents: Object.assign(listAccumulator.torrents, { + this.emit('PROCESS_TORRENT', processedTorrentDetailValues); + + return { [processedTorrentDetailValues.hash]: processedTorrentDetailValues, - }), - }; - }, - {id: Date.now(), torrents: {}} as {id: number; torrents: Torrents}, - ); + }; + }), + )), + ) as TorrentList; - this.emit('PROCESS_TORRENT_LIST_END', processedTorrentList); + const torrentListSummary = { + id: Date.now(), + torrents: processedTorrentList, + }; - return processedTorrentList; + this.emit('PROCESS_TORRENT_LIST_END', torrentListSummary); + + return torrentListSummary; } - processTransferRateResponse(transferRate: Array, {propLabels, valueTransformations}: MethodCallConfig) { + async processTransferRateResponse(transferRate: Array, configs: MethodCallConfigs) { this.emit('PROCESS_TRANSFER_RATE_START'); - return (transferRate.reduce((accumulator, value, index) => { - const key = propLabels[index]; - const transformValue = valueTransformations[index]; - - accumulator[key] = transformValue(value); - - return accumulator; - }, {} as Record) as unknown) as TransferSummary; + return Object.assign( + {}, + ...transferRate.map((value, index) => { + const {propLabel, transformValue} = configs[index]; + return { + [propLabel]: transformValue(value), + }; + }), + ) as TransferSummary; } testGateway(clientSettings?: Pick) { diff --git a/server/services/clientRequestManager.ts b/server/services/clientRequestManager.ts index 70559b30..5ed1c9ef 100644 --- a/server/services/clientRequestManager.ts +++ b/server/services/clientRequestManager.ts @@ -1,7 +1,7 @@ import BaseService from './BaseService'; import scgiUtil from '../util/scgiUtil'; -export type MultiMethodCalls = Array<{methodName: string; params: Array}>; +import type {MultiMethodCalls} from '../constants/rTorrentMethodCall'; class ClientRequestManager extends BaseService { isRequestPending = false; diff --git a/server/services/historyService.ts b/server/services/historyService.ts index 64fb95ff..548f2e1b 100644 --- a/server/services/historyService.ts +++ b/server/services/historyService.ts @@ -6,9 +6,8 @@ import BaseService from './BaseService'; import config from '../../config'; import HistoryEra from '../models/HistoryEra'; import historySnapshotTypes from '../../shared/constants/historySnapshotTypes'; -import methodCallUtil from '../util/methodCallUtil'; import objectUtil from '../../shared/util/objectUtil'; -import transferSummaryPropMap from '../constants/transferSummaryPropMap'; +import transferSummaryMethodCallConfigs from '../constants/transferSummaryMethodCallConfigs'; type HistorySnapshotEvents = { // TODO: Switch to string literal template type when TypeScript 4.1 is released. @@ -22,8 +21,6 @@ interface HistoryServiceEvents extends HistorySnapshotEvents { FETCH_TRANSFER_SUMMARY_ERROR: () => void; } -const transferSummaryMethodCallConfig = methodCallUtil.getMethodCallConfigFromPropMap(transferSummaryPropMap); - class HistoryService extends BaseService { errorCount = 0; pollTimeout?: NodeJS.Timeout; @@ -151,7 +148,7 @@ class HistoryService extends BaseService { } this.services?.clientGatewayService - .fetchTransferSummary(transferSummaryMethodCallConfig) + .fetchTransferSummary(transferSummaryMethodCallConfigs) .then(this.handleFetchTransferSummarySuccess.bind(this)) .catch(this.handleFetchTransferSummaryError.bind(this)); } diff --git a/server/services/taxonomyService.ts b/server/services/taxonomyService.ts index 3940197b..89d05151 100644 --- a/server/services/taxonomyService.ts +++ b/server/services/taxonomyService.ts @@ -4,7 +4,7 @@ import torrentStatusMap from '../../shared/constants/torrentStatusMap'; import type {Taxonomy, TaxonomyDiffs} from '../../shared/types/Taxonomy'; import type {TorrentStatus} from '../../shared/constants/torrentStatusMap'; -import type {TorrentProperties, Torrents} from '../../shared/types/Torrent'; +import type {TorrentProperties, TorrentList} from '../../shared/types/Torrent'; interface TaxonomyServiceEvents { TAXONOMY_DIFF_CHANGE: (payload: {id: number; diff: TaxonomyDiffs}) => void; @@ -74,7 +74,7 @@ class TaxonomyService extends BaseService { this.taxonomy.trackerCounts = {all: 0}; } - handleProcessTorrentListEnd({torrents}: {torrents: Torrents}) { + handleProcessTorrentListEnd({torrents}: {torrents: TorrentList}) { const {length} = Object.keys(torrents); this.taxonomy.statusCounts.all = length; diff --git a/server/services/torrentService.ts b/server/services/torrentService.ts index 22966dec..7f880b93 100644 --- a/server/services/torrentService.ts +++ b/server/services/torrentService.ts @@ -1,11 +1,10 @@ import {deepEqual} from 'fast-equals'; -import type {TorrentProperties, TorrentListDiff, Torrents} from '@shared/types/Torrent'; +import type {TorrentProperties, TorrentListDiff, TorrentListSummary} from '@shared/types/Torrent'; import BaseService from './BaseService'; import config from '../../config'; -import methodCallUtil from '../util/methodCallUtil'; -import torrentListPropMap from '../constants/torrentListPropMap'; +import torrentListMethodCallConfigs from '../constants/torrentListMethodCallConfigs'; import { getTorrentETAFromProperties, @@ -22,16 +21,11 @@ interface TorrentServiceEvents { removeListener: (event: keyof Omit) => void; } -const torrentListMethodCallConfig = methodCallUtil.getMethodCallConfigFromPropMap(torrentListPropMap); - class TorrentService extends BaseService { errorCount = 0; pollEnabled = false; pollTimeout: NodeJS.Timeout | null = null; - torrentListSummary: { - id: number; - torrents: Torrents; - } = {id: Date.now(), torrents: {}}; + torrentListSummary: TorrentListSummary = {id: Date.now(), torrents: {}}; constructor(...args: ConstructorParameters) { super(...args); @@ -147,7 +141,7 @@ class TorrentService extends BaseService { } return this.services?.clientGatewayService - .fetchTorrentList(torrentListMethodCallConfig) + .fetchTorrentList(torrentListMethodCallConfigs) .then(this.handleFetchTorrentListSuccess) .catch(this.handleFetchTorrentListError); } diff --git a/server/util/methodCallUtil.js b/server/util/methodCallUtil.js deleted file mode 100644 index e4568d81..00000000 --- a/server/util/methodCallUtil.js +++ /dev/null @@ -1,28 +0,0 @@ -const methodCallUtil = { - getMethodCallConfigFromPropMap(map, requestedKeys) { - let desiredKeys = Array.from(map.keys()); - - if (requestedKeys != null) { - desiredKeys = desiredKeys.filter((key) => requestedKeys.includes(key)); - } - - return desiredKeys.reduce( - (accumulator, key) => { - const {methodCall, transformValue} = map.get(key); - - accumulator.methodCalls.push(methodCall); - accumulator.propLabels.push(key); - accumulator.valueTransformations.push(transformValue); - - return accumulator; - }, - { - methodCalls: [], - propLabels: [], - valueTransformations: [], - }, - ); - }, -}; - -export default methodCallUtil; diff --git a/server/util/torrentPropertiesUtil.ts b/server/util/torrentPropertiesUtil.ts index 56f368c9..8c47696d 100644 --- a/server/util/torrentPropertiesUtil.ts +++ b/server/util/torrentPropertiesUtil.ts @@ -4,8 +4,12 @@ import truncateTo from './numberUtils'; import type {TorrentProperties} from '../../shared/types/Torrent'; import type {TorrentStatus} from '../../shared/constants/torrentStatusMap'; -export const getTorrentETAFromProperties = (torrentProperties: TorrentProperties) => { - const {downRate, bytesDone, sizeBytes} = torrentProperties; +export const getTorrentETAFromProperties = (processingTorrentProperties: Record) => { + const {downRate, bytesDone, sizeBytes} = processingTorrentProperties; + + if (typeof downRate !== 'number' || typeof bytesDone !== 'number' || typeof sizeBytes !== 'number') { + return Infinity; + } if (downRate > 0) { return formatUtil.secondsToDuration((sizeBytes - bytesDone) / downRate); @@ -14,8 +18,14 @@ export const getTorrentETAFromProperties = (torrentProperties: TorrentProperties return Infinity; }; -export const getTorrentPercentCompleteFromProperties = (torrentProperties: TorrentProperties) => { - const percentComplete = (torrentProperties.bytesDone / torrentProperties.sizeBytes) * 100; +export const getTorrentPercentCompleteFromProperties = (processingTorrentProperties: Record) => { + const {bytesDone, sizeBytes} = processingTorrentProperties; + + if (typeof bytesDone !== 'number' || typeof sizeBytes !== 'number') { + return 0; + } + + const percentComplete = (bytesDone / sizeBytes) * 100; if (percentComplete > 0 && percentComplete < 10) { return Number(truncateTo(percentComplete, 2)); @@ -27,8 +37,8 @@ export const getTorrentPercentCompleteFromProperties = (torrentProperties: Torre return percentComplete; }; -export const getTorrentStatusFromProperties = (torrentProperties: TorrentProperties) => { - const {isHashing, isComplete, isOpen, upRate, downRate, state, message} = torrentProperties; +export const getTorrentStatusFromProperties = (processingTorrentProperties: Record) => { + const {isHashing, isComplete, isOpen, upRate, downRate, state, message} = processingTorrentProperties; const torrentStatus: Array = []; @@ -50,7 +60,7 @@ export const getTorrentStatusFromProperties = (torrentProperties: TorrentPropert torrentStatus.push('stopped'); } - if (message.length) { + if (typeof message === 'string' && message.length) { torrentStatus.push('error'); } diff --git a/shared/types/ServerEvents.ts b/shared/types/ServerEvents.ts index d77c56d5..6701f0ef 100644 --- a/shared/types/ServerEvents.ts +++ b/shared/types/ServerEvents.ts @@ -1,7 +1,7 @@ import type {Disks} from './DiskUsage'; import type {NotificationCount} from './Notification'; import type {Taxonomy, TaxonomyDiffs} from './Taxonomy'; -import type {Torrents, TorrentListDiff} from './Torrent'; +import type {TorrentList, TorrentListDiff} from './Torrent'; import type {TransferHistory, TransferSummary, TransferSummaryDiff} from './TransferData'; // type: data @@ -13,7 +13,7 @@ export interface ServerEvents { NOTIFICATION_COUNT_CHANGE: NotificationCount; TAXONOMY_FULL_UPDATE: Taxonomy; TAXONOMY_DIFF_CHANGE: TaxonomyDiffs; - TORRENT_LIST_FULL_UPDATE: Torrents; + TORRENT_LIST_FULL_UPDATE: TorrentList; TORRENT_LIST_DIFF_CHANGE: TorrentListDiff; TRANSFER_HISTORY_FULL_UPDATE: TransferHistory; TRANSFER_SUMMARY_FULL_UPDATE: TransferSummary; diff --git a/shared/types/Torrent.ts b/shared/types/Torrent.ts index ce6be120..6974d81d 100644 --- a/shared/types/Torrent.ts +++ b/shared/types/Torrent.ts @@ -77,6 +77,11 @@ export interface TorrentListDiff { }; } -export interface Torrents { +export interface TorrentList { [hash: string]: TorrentProperties; } + +export interface TorrentListSummary { + id: number; + torrents: TorrentList; +}