Merge pull request #14 from lbryio/publish-cache

Make publishes appear immediately in My Files
This commit is contained in:
alexliebowitz 2017-03-28 02:26:45 -04:00 committed by GitHub
commit b3aa61cf47
4 changed files with 200 additions and 11 deletions

View file

@ -15,6 +15,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
* You can now make API calls directly on the lbry module, e.g. lbry.peer_list()
* New-style API calls return promises instead of using callbacks
* Wherever possible, use outpoints for unique IDs instead of names or SD hashes
* New publishes now display immediately in My Files, even before they hit the lbrynet file manager.
### Changed
* Update process now easier and more reliable

View file

@ -179,7 +179,7 @@ let FileActionsRow = React.createClass({
let linkBlock;
if (this.state.fileInfo === false && !this.state.attemptingDownload) {
linkBlock = <Link button="text" label="Download" icon="icon-download" onClick={this.onDownloadClick} />;
} else if (this.state.attemptingDownload || !this.state.fileInfo.completed) {
} else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) {
const
progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0,
label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...',

View file

@ -1,15 +1,103 @@
import lighthouse from './lighthouse.js';
import jsonrpc from './jsonrpc.js';
import {getLocal, setLocal} from './utils.js';
const {remote} = require('electron');
const menu = remote.require('./menu/main-menu');
/**
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to
* needed to make a dummy claim or file info object.
*/
function savePendingPublish(name) {
const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublish = {
claim_id: 'pending_claim_' + name,
txid: 'pending_' + name,
nout: 0,
outpoint: 'pending_' + name + ':0',
name: name,
time: Date.now(),
};
setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]);
return newPendingPublish;
}
function removePendingPublish({name, outpoint}) {
setLocal('pendingPublishes', getPendingPublishes().filter(
(pub) => pub.name != name && pub.outpoint != outpoint
));
}
/**
* Gets the current list of pending publish attempts. Filters out any that have timed out and
* removes them from the list.
*/
function getPendingPublishes() {
const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublishes = [];
for (let pendingPublish of pendingPublishes) {
if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) {
newPendingPublishes.push(pendingPublish);
}
}
setLocal('pendingPublishes', newPendingPublishes);
return newPendingPublishes
}
/**
* Gets a pending publish attempt by its name or (fake) outpoint. If none is found (or one is found
* but it has timed out), returns null.
*/
function getPendingPublish({name, outpoint}) {
const pendingPublishes = getPendingPublishes();
const pendingPublishIndex = pendingPublishes.findIndex(
({name: itemName, outpoint: itemOutpoint}) => itemName == name || itemOutpoint == outpoint
);
const pendingPublish = pendingPublishes[pendingPublishIndex];
if (pendingPublishIndex == -1) {
return null;
} else if (Date.now() - pendingPublish.time > lbry.pendingPublishTimeout) {
// Pending publish timed out, so remove it from the stored list and don't match
const newPendingPublishes = pendingPublishes.slice();
newPendingPublishes.splice(pendingPublishIndex, 1);
setLocal('pendingPublishes', newPendingPublishes);
return null;
} else {
return pendingPublish;
}
}
function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) {
return {
name: name,
outpoint: outpoint,
claim_id: claim_id,
txid: txid,
nout: nout,
};
}
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
return {
name: name,
outpoint: outpoint,
claim_id: claim_id,
metadata: "Attempting publication",
};
}
let lbry = {
isConnected: false,
rootPath: '.',
daemonConnectionString: 'http://localhost:5279/lbryapi',
webUiUri: 'http://localhost:5279',
peerListTimeout: 6000,
pendingPublishTimeout: 20 * 60 * 1000,
colors: {
primary: '#155B4A'
},
@ -251,18 +339,49 @@ lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0)
}, () => scheduleNextCheckOrTimeout());
}
/**
* Publishes a file. The optional fileListedCallback is called when the file becomes available in
* lbry.file_list() during the publish process.
*
* This currently includes a work-around to cache the file in local storage so that the pending
* publish can appear in the UI immediately.
*/
lbry.publish = function(params, fileListedCallback, publishedCallback, errorCallback) {
// Publishes a file.
// The optional fileListedCallback is called when the file becomes available in
// lbry.getFilesInfo() during the publish process.
lbry.call('publish', params, (result) => {
if (returnedPending) {
return;
}
// Use ES6 named arguments instead of directly passing param dict?
lbry.call('publish', params, publishedCallback, errorCallback);
if (fileListedCallback) {
lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
fileListedCallback(fileInfo);
});
}
clearTimeout(returnPendingTimeout);
publishedCallback(result);
}, (err) => {
if (returnedPending) {
return;
}
clearTimeout(returnPendingTimeout);
errorCallback(err);
});
let returnedPending = false;
// Give a short grace period in case publish() returns right away or (more likely) gives an error
const returnPendingTimeout = setTimeout(() => {
returnedPending = true;
if (publishedCallback) {
savePendingPublish(params.name);
publishedCallback(true);
}
if (fileListedCallback) {
savePendingPublish(params.name);
fileListedCallback(true);
}
}, 2000);
//lbry.getFileInfoWhenListed(params.name, function(fileInfo) {
// fileListedCallback(fileInfo);
//});
}
lbry.getVersionInfo = function(callback) {
@ -503,6 +622,60 @@ lbry.showMenuIfNeeded = function() {
sessionStorage.setItem('menuShown', chosenMenu);
};
/**
* Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs,
* these are designed to be transparent wrappers around the corresponding API methods.
*/
/**
* Returns results from the file_list API method, plus dummy entries for pending publishes.
* (If a real publish with the same name is found, the pending publish will be ignored and removed.)
*/
lbry.file_list = function(params={}) {
return new Promise((resolve, reject) => {
const {name, outpoint} = params;
/**
* If we're searching by outpoint, check first to see if there's a matching pending publish.
* Pending publishes use their own faux outpoints that are always unique, so we don't need
* to check if there's a real file.
*/
if (outpoint !== undefined) {
const pendingPublish = getPendingPublish({outpoint});
if (pendingPublish) {
resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
return;
}
}
lbry.call('file_list', params, (fileInfos) => {
// Remove any pending publications that are now listed in the file manager
const pendingPublishes = getPendingPublishes();
for (let {name: itemName} of fileInfos) {
if (pendingPublishes.find(() => name == itemName)) {
removePendingPublish({name: name});
}
}
const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo);
resolve([...fileInfos, ...dummyFileInfos]);
}, reject, reject);
});
}
lbry.claim_list_mine = function(params={}) {
return new Promise((resolve, reject) => {
lbry.call('claim_list_mine', params, (claims) => {
// Filter out pending publishes when the name is already in the file manager
const dummyClaims = getPendingPublishes().filter(
(pub) => !claims.find(({name}) => name == pub.name)
).map(pendingPublishToDummyClaim);
resolve([...claims, ...dummyClaims]);
}, reject, reject);
});
}
lbry = new Proxy(lbry, {
get: function(target, name) {
if (name in target) {

15
ui/js/utils.js Normal file
View file

@ -0,0 +1,15 @@
/**
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet.
*/
export function getLocal(key) {
const itemRaw = localStorage.getItem(key);
return itemRaw === null ? undefined : JSON.parse(itemRaw);
}
/**
* Thin wrapper around localStorage.setItem(). Converts value to JSON.
*/
export function setLocal(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}