diff --git a/CHANGELOG.md b/CHANGELOG.md index c179bc4b3..c9e0a3984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * There is no longer a minimum channel length (#645) * Changed the File page to make it clearer how to to open the folder for a file * The upgrade message is now friendlier and includes a link to the release notes. + * Improved Discover page load time by batching all URIs into one API call ### Fixed * Improve layout (and implementation) of the icon panel in file tiles and cards diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index f377a131c..912b06d11 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -24,68 +24,46 @@ const { ipcRenderer } = require("electron"); const DOWNLOAD_POLL_INTERVAL = 250; -export function doResolveUri(uri) { +export function doResolveUris(uris) { return function(dispatch, getState) { - uri = lbryuri.normalize(uri); - + uris = uris.map(lbryuri.normalize); const state = getState(); - const alreadyResolving = selectResolvingUris(state).indexOf(uri) !== -1; - if (!alreadyResolving) { - dispatch({ - type: types.RESOLVE_URI_STARTED, - data: { uri }, - }); - - lbry.resolve({ uri }).then(resolutionInfo => { - const { claim, certificate } = resolutionInfo - ? resolutionInfo - : { claim: null, certificate: null }; - - dispatch({ - type: types.RESOLVE_URI_COMPLETED, - data: { - uri, - claim, - certificate, - }, - }); - }); - } - }; -} - -export function doCancelResolveUri(uri) { - return function(dispatch, getState) { - uri = lbryuri.normalize(uri); - - const state = getState(); - const alreadyResolving = selectResolvingUris(state).indexOf(uri) !== -1; - - if (alreadyResolving) { - lbry.cancelResolve({ uri }); - dispatch({ - type: types.RESOLVE_URI_CANCELED, - data: { - uri, - }, - }); - } - }; -} - -export function doCancelAllResolvingUris() { - return function(dispatch, getState) { - const state = getState(); + // Filter out URIs that are already resolving const resolvingUris = selectResolvingUris(state); - const actions = []; + const urisToResolve = uris.filter(uri => !resolvingUris.includes(uri)); - resolvingUris.forEach(uri => actions.push(doCancelResolveUri(uri))); + if (urisToResolve.length === 0) { + return; + } - dispatch(batchActions(...actions)); + dispatch({ + type: types.RESOLVE_URIS_STARTED, + data: { uris }, + }); + + let resolveInfo = {}; + lbry.resolve({ uris: urisToResolve }).then(result => { + for (let [uri, uriResolveInfo] of Object.entries(result)) { + const { claim, certificate } = uriResolveInfo || { + claim: null, + certificate: null, + }; + resolveInfo[uri] = { claim, certificate }; + } + + dispatch({ + type: types.RESOLVE_URIS_COMPLETED, + data: { resolveInfo }, + }); + }); }; } +export function doResolveUri(uri) { + return doResolveUris([uri]); +} + export function doFetchFeaturedUris() { return function(dispatch, getState) { const state = getState(); @@ -96,28 +74,27 @@ export function doFetchFeaturedUris() { const success = ({ Categories, Uris }) => { let featuredUris = {}; - const actions = []; - + let urisToResolve = []; Categories.forEach(category => { if (Uris[category] && Uris[category].length) { const uris = Uris[category]; featuredUris[category] = uris; - uris.forEach(uri => { - actions.push(doResolveUri(uri)); - }); + urisToResolve = [...urisToResolve, ...uris]; } }); - actions.push({ - type: types.FETCH_FEATURED_CONTENT_COMPLETED, - data: { - categories: Categories, - uris: featuredUris, - success: true, + const actions = [ + doResolveUris(urisToResolve), + { + type: types.FETCH_FEATURED_CONTENT_COMPLETED, + data: { + categories: Categories, + uris: featuredUris, + success: true, + }, }, - }); - + ]; dispatch(batchActions(...actions)); }; diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 140509461..9a9143070 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -47,9 +47,8 @@ export const SUPPORT_TRANSACTION_FAILED = "SUPPORT_TRANSACTION_FAILED"; export const FETCH_FEATURED_CONTENT_STARTED = "FETCH_FEATURED_CONTENT_STARTED"; export const FETCH_FEATURED_CONTENT_COMPLETED = "FETCH_FEATURED_CONTENT_COMPLETED"; -export const RESOLVE_URI_STARTED = "RESOLVE_URI_STARTED"; -export const RESOLVE_URI_COMPLETED = "RESOLVE_URI_COMPLETED"; -export const RESOLVE_URI_CANCELED = "RESOLVE_URI_CANCELED"; +export const RESOLVE_URIS_STARTED = "RESOLVE_URIS_STARTED"; +export const RESOLVE_URIS_COMPLETED = "RESOLVE_URIS_COMPLETED"; export const FETCH_CHANNEL_CLAIMS_STARTED = "FETCH_CHANNEL_CLAIMS_STARTED"; export const FETCH_CHANNEL_CLAIMS_COMPLETED = "FETCH_CHANNEL_CLAIMS_COMPLETED"; export const FETCH_CHANNEL_CLAIM_COUNT_STARTED = diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 36f221afb..3d6c498a5 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -311,30 +311,24 @@ lbry.claim_list_mine = function(params = {}) { }); }; -lbry._resolveXhrs = {}; lbry.resolve = function(params = {}) { return new Promise((resolve, reject) => { - if (!params.uri) { - throw __("Resolve has hacked cache on top of it that requires a URI"); - } - lbry._resolveXhrs[params.uri] = apiCall( + apiCall( "resolve", params, function(data) { - resolve(data && data[params.uri] ? data[params.uri] : {}); + if ("uri" in params) { + // If only a single URI was requested, don't nest the results in an object + resolve(data && data[params.uri] ? data[params.uri] : {}); + } else { + resolve(data || {}); + } }, reject ); }); }; -lbry.cancelResolve = function(params = {}) { - const xhr = lbry._resolveXhrs[params.uri]; - if (xhr && xhr.readyState > 0 && xhr.readyState < 4) { - xhr.abort(); - } -}; - lbry = new Proxy(lbry, { get: function(target, name) { if (name in target) { diff --git a/ui/js/page/discover/index.js b/ui/js/page/discover/index.js index be49b892d..01f9add5e 100644 --- a/ui/js/page/discover/index.js +++ b/ui/js/page/discover/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doFetchFeaturedUris, doCancelAllResolvingUris } from "actions/content"; +import { doFetchFeaturedUris } from "actions/content"; import { selectFeaturedUris, selectFetchingFeaturedUris, @@ -14,7 +14,6 @@ const select = state => ({ const perform = dispatch => ({ fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()), - cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()), }); export default connect(select, perform)(DiscoverPage); diff --git a/ui/js/page/discover/view.jsx b/ui/js/page/discover/view.jsx index bc86a9597..19e00980d 100644 --- a/ui/js/page/discover/view.jsx +++ b/ui/js/page/discover/view.jsx @@ -204,10 +204,6 @@ class DiscoverPage extends React.PureComponent { this.props.fetchFeaturedUris(); } - componentWillUnmount() { - this.props.cancelResolvingUris(); - } - render() { const { featuredUris, fetchingFeaturedUris } = this.props; const hasContent = diff --git a/ui/js/page/fileListDownloaded/index.js b/ui/js/page/fileListDownloaded/index.js index 449d3f196..2da37ca9c 100644 --- a/ui/js/page/fileListDownloaded/index.js +++ b/ui/js/page/fileListDownloaded/index.js @@ -11,7 +11,6 @@ import { } from "selectors/claims"; import { doFetchClaimListMine } from "actions/content"; import { doNavigate } from "actions/navigation"; -import { doCancelAllResolvingUris } from "actions/content"; import FileListDownloaded from "./view"; const select = state => ({ @@ -25,7 +24,6 @@ const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), fetchFileInfosDownloaded: () => dispatch(doFetchFileInfosAndPublishedClaims()), - cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()), fetchClaims: () => dispatch(doFetchClaimListMine()), }); diff --git a/ui/js/page/fileListDownloaded/view.jsx b/ui/js/page/fileListDownloaded/view.jsx index d970002ac..ceb0837e8 100644 --- a/ui/js/page/fileListDownloaded/view.jsx +++ b/ui/js/page/fileListDownloaded/view.jsx @@ -11,10 +11,6 @@ class FileListDownloaded extends React.PureComponent { if (!this.props.isFetching) this.props.fetchFileInfosDownloaded(); } - componentWillUnmount() { - this.props.cancelResolvingUris(); - } - render() { const { fileInfos, isFetching, navigate } = this.props; diff --git a/ui/js/page/fileListPublished/index.js b/ui/js/page/fileListPublished/index.js index c6bf2a43a..5b04e101e 100644 --- a/ui/js/page/fileListPublished/index.js +++ b/ui/js/page/fileListPublished/index.js @@ -8,7 +8,6 @@ import { } from "selectors/claims"; import { doClaimRewardType } from "actions/rewards"; import { doNavigate } from "actions/navigation"; -import { doCancelAllResolvingUris } from "actions/content"; import FileListPublished from "./view"; const select = state => ({ @@ -21,7 +20,6 @@ const perform = dispatch => ({ fetchClaims: () => dispatch(doFetchClaimListMine()), claimFirstPublishReward: () => dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)), - cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()), }); export default connect(select, perform)(FileListPublished); diff --git a/ui/js/page/fileListPublished/view.jsx b/ui/js/page/fileListPublished/view.jsx index 889fe8c70..0d9e093c8 100644 --- a/ui/js/page/fileListPublished/view.jsx +++ b/ui/js/page/fileListPublished/view.jsx @@ -14,10 +14,6 @@ class FileListPublished extends React.PureComponent { // if (this.props.claims.length > 0) this.props.fetchClaims(); } - componentWillUnmount() { - this.props.cancelResolvingUris(); - } - render() { const { claims, isFetching, navigate } = this.props; diff --git a/ui/js/page/show/view.jsx b/ui/js/page/show/view.jsx index 8b1e05789..3dd48a705 100644 --- a/ui/js/page/show/view.jsx +++ b/ui/js/page/show/view.jsx @@ -6,17 +6,15 @@ import FilePage from "page/file"; class ShowPage extends React.PureComponent { componentWillMount() { - const { isResolvingUri, resolveUri, uri } = this.props; + const { resolveUri, uri } = this.props; - if (!isResolvingUri) resolveUri(uri); + resolveUri(uri); } componentWillReceiveProps(nextProps) { - const { isResolvingUri, resolveUri, claim, uri } = nextProps; + const { resolveUri, uri } = nextProps; - if (!isResolvingUri && claim === undefined && uri) { - resolveUri(uri); - } + resolveUri(uri); } render() { diff --git a/ui/js/reducers/claims.js b/ui/js/reducers/claims.js index 7f883a4ea..a3e4ae2b4 100644 --- a/ui/js/reducers/claims.js +++ b/ui/js/reducers/claims.js @@ -4,26 +4,27 @@ const reducers = {}; const defaultState = {}; -reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) { - const { uri, certificate, claim } = action.data; - +reducers[types.RESOLVE_URIS_COMPLETED] = function(state, action) { + const { resolveInfo } = action.data; const byUri = Object.assign({}, state.claimsByUri); const byId = Object.assign({}, state.byId); - if (claim) { - byId[claim.claim_id] = claim; - byUri[uri] = claim.claim_id; - } else if (claim === undefined && certificate !== undefined) { - byId[certificate.claim_id] = certificate; - // Don't point URI at the channel certificate unless it actually is - // a channel URI. This is brittle. - if (!uri.split(certificate.name)[1].match(/\//)) { - byUri[uri] = certificate.claim_id; + for (let [uri, { certificate, claim }] of Object.entries(resolveInfo)) { + if (claim) { + byId[claim.claim_id] = claim; + byUri[uri] = claim.claim_id; + } else if (claim === undefined && certificate !== undefined) { + byId[certificate.claim_id] = certificate; + // Don't point URI at the channel certificate unless it actually is + // a channel URI. This is brittle. + if (!uri.split(certificate.name)[1].match(/\//)) { + byUri[uri] = certificate.claim_id; + } else { + byUri[uri] = null; + } } else { byUri[uri] = null; } - } else { - byUri[uri] = null; } return Object.assign({}, state, { diff --git a/ui/js/reducers/content.js b/ui/js/reducers/content.js index 56195e917..5afb2790a 100644 --- a/ui/js/reducers/content.js +++ b/ui/js/reducers/content.js @@ -31,34 +31,23 @@ reducers[types.FETCH_REWARD_CONTENT_COMPLETED] = function(state, action) { }); }; -reducers[types.RESOLVE_URI_STARTED] = function(state, action) { - const { uri } = action.data; +reducers[types.RESOLVE_URIS_STARTED] = function(state, action) { + let { uris } = action.data; const oldResolving = state.resolvingUris || []; const newResolving = Object.assign([], oldResolving); - if (newResolving.indexOf(uri) === -1) newResolving.push(uri); + + for (let uri of uris) { + if (!newResolving.includes(uri)) { + newResolving.push(uri); + } + } return Object.assign({}, state, { resolvingUris: newResolving, }); }; -reducers[types.RESOLVE_URI_CANCELED] = reducers[ - types.RESOLVE_URI_COMPLETED -] = function(state, action) { - const { uri } = action.data; - const resolvingUris = state.resolvingUris; - const index = state.resolvingUris.indexOf(uri); - const newResolvingUris = [ - ...resolvingUris.slice(0, index), - ...resolvingUris.slice(index + 1), - ]; - - return Object.assign({}, state, { - resolvingUris: newResolvingUris, - }); -}; - reducers[types.SET_PLAYING_URI] = (state, action) => { return Object.assign({}, state, { playingUri: action.data.uri,