Stop Status and Info changes to support better status indicators and heartbeat on HTTP

This commit is contained in:
Mitra Ardron 2019-06-26 20:42:37 +10:00
parent b137ab82c1
commit 9d9e998d77
6 changed files with 118 additions and 71 deletions

7
API.md
View File

@ -109,10 +109,11 @@ Code|Name|Means
3|STATUS_LOADED|Code loaded but havent tried to connect 3|STATUS_LOADED|Code loaded but havent tried to connect
4|STATUS_PAUSED|It was launched, probably connected, but now paused so will be ignored by validfor() 4|STATUS_PAUSED|It was launched, probably connected, but now paused so will be ignored by validfor()
##### async p_stop(refreshstatus) ##### async stop(refreshstatus, cb)
Stop the transport, Stop the transport,
``` ```
refreshstatus (optional) callback(transport instance) to the UI to update status on display refreshstatus (optional) callback(transport instance) to the UI to update status on display
cb(err, this)
``` ```
### Transport: General storage and retrieval of objects ### Transport: General storage and retrieval of objects
##### p_rawstore(data) ##### p_rawstore(data)
@ -286,8 +287,8 @@ throw TransportError if invalid URL
##### validFor(url, func, opts) ##### validFor(url, func, opts)
True if the url and/or function is supported and the Transport is connected appropriately. True if the url and/or function is supported and the Transport is connected appropriately.
##### p_info() ##### p_info() and info(cb)
Return a JSON with info about the server. Return a JSON with info about the server via promise or callback
## Transports class ## Transports class
The Transports Class manages multiple transports The Transports Class manages multiple transports

View File

@ -54,15 +54,11 @@ class Transport {
return t.p_setup2(cb); // And connect return t.p_setup2(cb); // And connect
} }
/* Disconnect from the transport service - there is no guarrantee that a restart will be successfull so this is usually only for when exiting */ /* Disconnect from the transport service - there is no guarrantee that a restart will be successfull so this is usually only for when exiting */
p_stop(refreshstatus) { stop(refreshstatus, cb) {
// refreshstatus(Transport instance) => optional callback to the UI to update the status on the display // refreshstatus(Transport instance) => optional callback to the UI to update the status on the display
return new Promise((resolve, reject) => { this.status = Transport.STATUS_FAILED;
this.status = Transport.STATUS_FAILED; if (refreshstatus) refreshstatus(this);
if (refreshstatus) refreshstatus(this); cb(null, this);
//if (err) { reject(err) } else {
resolve();
//}
});
} }
togglePaused(cb) { togglePaused(cb) {
/* /*

View File

@ -3,12 +3,13 @@ const Transports = require('./Transports'); // Manage all Transports that are lo
const httptools = require('./httptools'); // Expose some of the httptools so that IPFS can use it as a backup const httptools = require('./httptools'); // Expose some of the httptools so that IPFS can use it as a backup
const Url = require('url'); const Url = require('url');
const stream = require('readable-stream'); const stream = require('readable-stream');
const debughttp = require('debug')('dweb-transports:http'); const debug = require('debug')('dweb-transports:http');
const stringify = require('canonical-json'); const stringify = require('canonical-json');
defaulthttpoptions = { defaulthttpoptions = {
urlbase: 'https://dweb.me' urlbase: 'https://dweb.me',
heartbeat: { delay: 30000 } // By default check twice a minute
}; };
servercommands = { // What the server wants to see to return each of these servercommands = { // What the server wants to see to return each of these
@ -25,6 +26,16 @@ servercommands = { // What the server wants to see to return each of these
class TransportHTTP extends Transport { class TransportHTTP extends Transport {
/* Subclass of Transport for handling HTTP - see API.md for docs
options {
urlbase: e.g. https://dweb.me Where to go for URLS like /arc/...
heartbeat: {
delay // Time in milliseconds between checks - 30000 might be appropriate - if missing it wont do a heartbeat
statusCB // Callback cb(transport) when status changes
}
}
*/
constructor(options) { constructor(options) {
super(options); // These are now options.http super(options); // These are now options.http
@ -54,26 +65,57 @@ class TransportHTTP extends Transport {
throw err; throw err;
} }
} }
async p_setup1(cb) {
this.status = Transport.STATUS_STARTING; p_setup1(statusCB) {
if (cb) cb(this); return new Promise((resolve, unusedReject) => {
await this.p_status(); this.status = Transport.STATUS_STARTING;
if (cb) cb(this); if (statusCB) statusCB(this);
return this; this.updateStatus((unusedErr, unusedRes) => {
if (statusCB) statusCB(this);
this.startHeartbeat(this.options.heartbeat);
resolve(this); // Note always resolve even if error from p_status as have set status to failed
});
})
} }
async p_status() { async p_status(cb) { //TODO-API
/* /*
Return a numeric code for the status of a transport. Return (via cb or promise) a numeric code for the status of a transport.
*/ */
try { if (cb) { try { this.updateStatus(cb) } catch(err) { cb(err)}} else { return new Promise((resolve, reject) => { try { this.updateStatus((err, res) => { if (err) {reject(err)} else {resolve(res)} })} catch(err) {reject(err)}})} // Promisify pattern v2f
this.info = await this.p_info(); }
this.status = Transport.STATUS_CONNECTED; updateStatus(cb) { //TODO-API
} catch(err) { this.updateInfo((err, res) => {
console.error(this.name, ": Error in p_status.info",err.message); if (err) {
this.status = Transport.STATUS_FAILED; debug("Error status call to info failed %s", err.message);
this.status = Transport.STATUS_FAILED;
cb(null, this.status); // DOnt pass error up, the status indicates the error
} else {
this.info = res; // Save result
this.status = Transport.STATUS_CONNECTED;
cb(null, this.status);
}
});
}
startHeartbeat({delay=undefined, statusCB=undefined}) {
if (delay) {
this.heartbeatTimer = setInterval(() => {
this.updateStatus((err, res)=>{ // Pings server and sets status
if (statusCB) statusCB(this); // repeatedly call callback if supplies
}, (unusedErr, unusedRes)=>{}); // Dont wait for status to complete
}, delay);
} }
return super.p_status(); }
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.hearbeatTimer);}
}
stop(refreshstatus, cb) {
this.stopHeartbeat();
this.status = Transport.STATUS_FAILED;
if (refreshstatus) { refreshstatus(this); }
cb(null, this);
} }
_cmdurl(command) { _cmdurl(command) {
@ -180,7 +222,7 @@ class TransportHTTP extends Transport {
:throws: TransportError if url invalid - note this happens immediately, not as a catch in the promise :throws: TransportError if url invalid - note this happens immediately, not as a catch in the promise
*/ */
//Logged by Transports //Logged by Transports
//debughttp("p_f_createreadstream %s", Url.parse(url).href); //debug("p_f_createreadstream %s", Url.parse(url).href);
try { try {
let self = this; let self = this;
if (wanturl) { if (wanturl) {
@ -206,7 +248,7 @@ class TransportHTTP extends Transport {
*/ */
// This breaks in browsers ... as 's' doesn't have .pipe but has .pipeTo and .pipeThrough neither of which work with stream.PassThrough // This breaks in browsers ... as 's' doesn't have .pipe but has .pipeTo and .pipeThrough neither of which work with stream.PassThrough
// TODO See https://github.com/nodejs/readable-stream/issues/406 in case its fixed in which case enable createReadStream in constructor above. // TODO See https://github.com/nodejs/readable-stream/issues/406 in case its fixed in which case enable createReadStream in constructor above.
debughttp("createreadstream %s %o", Url.parse(url).href, opts); debug("createreadstream %s %o", Url.parse(url).href, opts);
let through; let through;
through = new stream.PassThrough(); through = new stream.PassThrough();
httptools.p_GET(this._url(url, servercommands.rawfetch), Object.assign({wantstream: true}, opts)) httptools.p_GET(this._url(url, servercommands.rawfetch), Object.assign({wantstream: true}, opts))
@ -233,7 +275,7 @@ class TransportHTTP extends Transport {
:param opts: { start: byte to start from; end: optional end byte } :param opts: { start: byte to start from; end: optional end byte }
:resolves to stream: The readable stream. :resolves to stream: The readable stream.
*/ */
debughttp("createreadstream %s %o", Url.parse(url).href, opts); debug("createreadstream %s %o", Url.parse(url).href, opts);
try { try {
return await httptools.p_GET(this._url(url, servercommands.rawfetch), Object.assign({wantstream: true}, opts)); return await httptools.p_GET(this._url(url, servercommands.rawfetch), Object.assign({wantstream: true}, opts));
} catch(err) { } catch(err) {
@ -270,7 +312,7 @@ class TransportHTTP extends Transport {
async p_set(url, keyvalues, value) { // url = yjs:/yjs/database/table/key async p_set(url, keyvalues, value) { // url = yjs:/yjs/database/table/key
if (!url || !keyvalues) throw new errors.CodingError("TransportHTTP.p_set: invalid parms", url, keyvalyes); if (!url || !keyvalues) throw new errors.CodingError("TransportHTTP.p_set: invalid parms", url, keyvalyes);
// Logged by Transports // Logged by Transports
//debughttp("p_set %o %o %o", url, keyvalues, value); //debug("p_set %o %o %o", url, keyvalues, value);
if (typeof keyvalues === "string") { if (typeof keyvalues === "string") {
let data = stringify([{key: keyvalues, value: value}]); let data = stringify([{key: keyvalues, value: value}]);
await httptools.p_POST(this._url(url, servercommands.set), {data, contenttype: "application/json"}); // Returns immediately await httptools.p_POST(this._url(url, servercommands.set), {data, contenttype: "application/json"}); // Returns immediately
@ -309,11 +351,20 @@ class TransportHTTP extends Transport {
return { return {
table: "keyvaluetable", table: "keyvaluetable",
_map: await this.p_getall(url) _map: await this.p_getall(url)
}; // Data struc is ok as SmartDict.p_fetch will pass to KVT constructor }; // Data structure is ok as SmartDict.p_fetch will pass to KVT constructor
} }
*/ */
p_info() { return httptools.p_GET(`${this.urlbase}/info`, {retries: 5}); } // Try info, but dont wait more than approx 10secs async p_info() { //TODO-API
/*
Return (via cb or promise) a numeric code for the status of a transport.
*/
return new Promise((resolve, reject) => { try { this.updateInfo((err, res) => { if (err) {reject(err)} else {resolve(res)} })} catch(err) {reject(err)}}) // Promisify pattern v2b (no CB)
}
updateInfo(cb) {
httptools.p_GET(`${this.urlbase}/info`, {retries: 1}, cb); // Try info, but dont retry (usually heartbeat will reconnect)
}
static async p_test(opts={}) { static async p_test(opts={}) {
{console.log("TransportHTTP.test")} {console.log("TransportHTTP.test")}

View File

@ -183,21 +183,19 @@ class TransportIPFS extends Transport {
return this; return this;
} }
p_stop(refreshstatus) { stop(refreshstatus, cb) { //TODO-API p_stop > stop
return new Promise((resolve, reject) => { if (this.ipfstype === "client") {
if (this.ipfstype === "client") { this.ipfs.stop((err, res) => {
this.ipfs.stop((err, res) => {
this.status = Transport.STATUS_FAILED;
if (refreshstatus) refreshstatus(this);
if (err) { reject(err); } else { resolve(res); }
});
} else {
// We didn't start it, don't try and stop it
this.status = Transport.STATUS_FAILED; this.status = Transport.STATUS_FAILED;
if (refreshstatus) refreshstatus(this); if (refreshstatus) refreshstatus(this);
resolve(this); cb(err, res);
} });
}) } else {
// We didn't start it, don't try and stop it
this.status = Transport.STATUS_FAILED;
if (refreshstatus) refreshstatus(this);
cb(miull, this);
}
} }
async p_status() { async p_status() {
/* /*

View File

@ -83,20 +83,17 @@ class TransportWEBTORRENT extends Transport {
return this; return this;
} }
p_stop(refreshstatus) { stop(refreshstatus, cb) {
return new Promise((resolve, reject) => { this.webtorrent.destroy((err) => {
this.webtorrent.destroy((err) => { this.status = Transport.STATUS_FAILED;
this.status = Transport.STATUS_FAILED; if (refreshstatus) refreshstatus(this);
if (refreshstatus) refreshstatus(this); if (err) {
if (err) { debug("Webtorrent error during stopping %o", err);
debug("Webtorrent error during stopping %o", err); } else {
reject(err); debug("Webtorrent stopped");
} else { }
debug("Webtorrent stopped"); cb(err, this);
resolve(); });
}
});
})
} }
async p_status() { async p_status() {

View File

@ -4,6 +4,7 @@ const utils = require('./utils');
const debug = require('debug')('dweb-transports'); const debug = require('debug')('dweb-transports');
const httptools = require('./httptools'); const httptools = require('./httptools');
const each = require('async/each'); const each = require('async/each');
const map = require('async/map');
class Transports { class Transports {
/* /*
@ -41,7 +42,7 @@ class Transports {
/* /*
resolves to: a dictionary of statuses of transports, e.g. { TransportHTTP: STATUS_CONNECTED } resolves to: a dictionary of statuses of transports, e.g. { TransportHTTP: STATUS_CONNECTED }
*/ */
const res = this._transports.map((t) => { return {"name": t.name, "status": t.status}}) const res = Transports._transports.map((t) => { return {"name": t.name, "status": t.status}})
if (cb) { cb(null, res)} else { return new Promise((resolve, reject) => resolve(res))} if (cb) { cb(null, res)} else { return new Promise((resolve, reject) => resolve(res))}
} }
static validFor(urls, func, opts) { //TODO-RELOAD check for noCache support static validFor(urls, func, opts) { //TODO-RELOAD check for noCache support
@ -673,15 +674,18 @@ class Transports {
})); }));
if (cb) { prom.catch((err) => cb(err)).then((res)=>cb(null,res)); } else { return prom; } // This should be a standard unpromisify pattern if (cb) { prom.catch((err) => cb(err)).then((res)=>cb(null,res)); } else { return prom; } // This should be a standard unpromisify pattern
} }
static p_stop(refreshstatus, cb) { static p_stop(refreshstatus, cb) { //TODO-API cb
if (cb) { try { f.call(this, cb) } catch(err) { cb(err)}} else { return new Promise((resolve, reject) => { try { f.call(this, (err, res) => { if (err) {reject(err)} else {resolve(res)} })} catch(err) {reject(err)}})} // Promisify pattern v2
/* Disconnect from all services, may not be able to reconnect */ /* Disconnect from all services, may not be able to reconnect */
//TODO rewrite with async/map
const prom = Promise.all(this._connected() function f(cb) {
.map((t) => { map(this._connected(),
debug("Stopping %s", t.name); (t, cb2) => {
return t.p_stop(refreshstatus); debug("Stopping %s", t.name);
})); t.stop(refreshstatus, cb2);
if (cb) { prom.catch((err) => cb(err)).then((res)=>cb(null,res)); } else { return prom; } // This should be a standard unpromisify pattern },
cb);
}
} }
static async refreshstatus(t) { static async refreshstatus(t) {