// Copyright 2018 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under
// the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT> or
// the Modified BSD license <LICENSE-BSD or https://opensource.org/licenses/BSD-3-Clause>,
// at your option.
//
// This file may not be copied, modified, or distributed except according to those terms.
//
// Please review the Licences for the specific language governing permissions and limitations
// relating to use of the SAFE Network Software.
const { EventEmitter } = require('events');
const { autoref } = require('./helpers');
const api = require('./api');
const lib = require('./native/lib');
const consts = require('./consts');
const errConst = require('./error_const');
const makeError = require('./native/_error.js');
const { webFetch, fetch } = require('./web_fetch.js');
const { EXPOSE_AS_EXPERIMENTAL_API } = require('./helpers');
const EXPERIMENTAL_APIS = ['web'];
/**
* @private
* Validates appInfo and properly handles error
*/
const validateAppInfo = (_appInfo) => {
const appInfo = _appInfo;
const appInfoMustHaveProperties = ['id', 'name', 'vendor'];
const hasCorrectProperties = appInfoMustHaveProperties.every((prop) => {
if (appInfo && appInfo[prop]) {
appInfo[prop] = appInfo[prop].trim();
return Object.prototype.hasOwnProperty.call(appInfo, prop) && appInfo[prop];
}
return false;
});
if (!hasCorrectProperties) {
throw makeError(errConst.MALFORMED_APP_INFO.code, errConst.MALFORMED_APP_INFO.msg);
}
};
/**
* @private
* Init logging on the underlying library only if it wasn't done already
*/
const initLogging = (appInfo, options) => {
if (options.log && !SAFEApp.logFilePath) {
let filename = `${appInfo.name}.${appInfo.vendor}`.replace(/[^\w\d_\-.]/g, '_');
filename = `${filename}.log`;
return lib.app_init_logging(filename)
.then(() => lib.app_output_log_path(filename))
.then((logPath) => { SAFEApp.logFilePath = logPath; })
.catch((err) => {
throw makeError(errConst.LOGGER_INIT_ERROR.code, errConst.LOGGER_INIT_ERROR.msg(err));
});
}
};
/**
* @private
* Set additional search path for the config files if it was requested in
* the options. E.g. log.toml and crust.config files will be search
* in this additional search path.
*/
const setSearchPath = (options) => {
if (options.configPath) {
return lib.app_set_additional_search_path(options.configPath)
.catch((err) => {
throw makeError(errConst.CONFIG_PATH_ERROR.code, errConst.CONFIG_PATH_ERROR.msg(err));
});
}
};
/**
* Holds a session with the network and is the primary interface to interact
* with the network
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const networkStateCallback = (state) => {
* console.log('Network state change event: ', state);
* };
*
* const initialisationOptions = {
* log : true,
* registerScheme : false
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(
* appInfo,
* networkStateCallBack,
* initialisationOptions
* );
* } catch (err) {
* throw err;
* }
* };
*/
class SAFEApp extends EventEmitter {
/**
* @hideconstructor
* Initiate a new SAFEApp instance. Wire up all the API's and set up the
* authentication URI-handler with the system.
*
* @param {AppInfo} appInfo
* @param {Function} [networkStateCallBack=null] optional callback function
* to receive network state updates
* @param {InitOptions} [options] initilalisation options
*/
constructor(appInfo, networkStateCallBack, options) {
super();
validateAppInfo(appInfo);
this.options = Object.assign({
log: true,
registerScheme: true,
configPath: null,
forceUseMock: false,
enableExperimentalApis: false,
}, options);
if (typeof this.options.forceUseMock !== 'boolean') {
throw new Error('The \'forceUseMock\' option must be a boolean.');
}
if (typeof this.options.enableExperimentalApis !== 'boolean') {
throw new Error('The \'enableExperimentalApis\' option must be a boolean.');
}
lib.init(this.options);
this._appInfo = appInfo;
this.networkState = consts.NET_STATE_INIT;
if (networkStateCallBack) {
this._networkStateCallBack = networkStateCallBack;
}
this.connection = null;
Object.getOwnPropertyNames(api).forEach((key) => {
this[`_${key}`] = new api[key](this);
if (EXPERIMENTAL_APIS.includes(key)) {
Object.getOwnPropertyNames(api[key].prototype).forEach((experimentalApi) => {
if (experimentalApi === 'constructor') {
// dont mess with this...
return;
}
const cachedProtoProp = this[`_${key}`][experimentalApi].bind(this[`_${key}`]);
this[`_${key}`][experimentalApi] = (...args) => EXPOSE_AS_EXPERIMENTAL_API.call(this, cachedProtoProp, ...args);
});
}
});
}
async init() {
await initLogging(this.appInfo, this.options);
await setSearchPath(this.options);
}
/**
* Get an {@link AuthInterface} instance
* @returns {AuthInterface}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const auth = app.auth;
* } catch (err) {
* throw err;
* }
* };
*/
get auth() {
return this._auth;
}
/**
* Get a {@link WebInterface} interface
* @returns {WebInterface}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const web = app.web;
* } catch (err) {
* throw err;
* }
* };
*/
get web() {
return this._web;
}
/**
* Get a {@link CryptoInterface} interface
* @returns {CryptoInterface}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const crypto = app.crypto;
* } catch (err) {
* throw err;
* }
* };
*/
get crypto() {
return this._crypto;
}
/**
* Get a {@link CipherOptInterface} interface
* @returns {CipherOptInterface}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const cipherOpt = app.cipherOpt;
* } catch (err) {
* throw err;
* }
* };
*/
get cipherOpt() {
return this._cipherOpt;
}
/**
* Get an {@link ImmutableDataInterface}
* @returns {ImmutableDataInterface}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const immutableData = app.immutableData;
* } catch (err) {
* throw err;
* }
* };
*/
get immutableData() {
return this._immutableData;
}
/**
* Get a {@link MutableDataInterface}
* @returns {MutableDataInterface}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const mutableData = app.mutableData;
* } catch (err) {
* throw err;
* }
* };
*/
get mutableData() {
return this._mutableData;
}
/**
* Function to lookup a given `safe://`-URL in accordance with the
* public name resolution and find the requested network resource.
*
* @param {String} url the url you want to fetch
* @param {WebFetchOptions} [options=null] additional options
* @throws {ERR_SERVICE_NOT_FOUND|ERR_NO_SUCH_DATA|ERR_CONTENT_NOT_FOUND
* |ERR_NO_SUCH_ENTRY|ERR_FILE_NOT_FOUND|MISSING_URL|INVALID_URL}
* @returns {Promise<{ body: Buffer, headers: Object }>}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* const app = await safe.initialiseApp(appInfo);
* const unRegisteredUri = await app.auth.genConnUri();
* await app.auth.loginFromUri(unRegisteredUri);
* const webFetchOptions = {
* range: {
* start:safe.CONSTANTS.NFS_FILE_START,
* end: safe.CONSTANTS.NFS_FILE_END
* }
* };
* try {
* const data = await app.webFetch(
* 'safe://home.safenetwork',
* webFetchOptions
* );
* // Alternatively, fetch an ImmutableData XOR-URL such as:
* // safe://hygkdkftyhkmzma5cjwgcghws9hyorcucqyqna1uaje68hyquah7nd9kh3rjy
* } catch(err) {
* throw err;
* }
* };
*/
webFetch(url, options) {
return webFetch.call(this, url, options);
}
/**
* Experimental function to lookup a given `safe://`-URL in accordance with the
* public name resolution and find the requested network resource.
*
* @param {String} url the url you want to fetch
* @throws {ERR_SERVICE_NOT_FOUND|ERR_NO_SUCH_DATA|ERR_CONTENT_NOT_FOUND
* |ERR_NO_SUCH_ENTRY|ERR_FILE_NOT_FOUND|MISSING_URL|INVALID_URL}
* @returns {Promise<NetworkResource>} the network resource found from the passed URL
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* // If you have an XOR-URL with a type tag, and therefore represents MutableData,
* // use this operation to fetch an interface to the underlying data structure.
* try {
* const app = await safe.initialiseApp(appInfo);
* const unRegisteredUri = await app.auth.genConnUri();
* await app.auth.loginFromUri(unRegisteredUri);
* const data = await app.fetch(
* 'safe://hyfktcerbwpctjz6ws8468hncw1ddpzrz65z3mapzx9wr413r7gj3w6yt5y:15001'
* );
* } catch(err) {
* throw err;
* }
* };
*/
fetch(url) {
return fetch.call(this, url);
}
/**
* @private
* Replace the connection to the native layer. When there is already one
* set up for the current app, free it on the native layer. Should only be
* used at startup/beginning as it will devaluate all handlers that might
* still be around after switching.
*
* @param {Pointer} conn the pointer to the native object
*/
set connection(conn) {
if (this._connection) {
lib.app_free(this._connection);
}
this._connection = conn;
}
/**
* Returns pointer to current connection object held in memory.
* @throws {SETUP_INCOMPLETE}
* @returns {Pointer}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const connection = app.connection;
* } catch(err) {
* throw err;
* }
* };
*/
get connection() {
if (!this._connection) {
throw makeError(errConst.SETUP_INCOMPLETE.code, errConst.SETUP_INCOMPLETE.msg);
}
return this._connection;
}
/**
* @private
* Set the new network state based on the state code provided.
*
* @param {Number} state
*/
set networkState(state) {
this._networkState = state;
}
/**
* Textual representation of the current network connection state.
*
* @returns {String} current network connection state
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const networkState = app.networkState;
* } catch(err) {
* throw err;
* }
* };
*/
get networkState() {
// Although it should never happen, if the state code is invalid
// we return the current network conn state as 'Unknown'.
let currentState = 'Unknown';
switch (this._networkState) {
case consts.NET_STATE_INIT:
currentState = 'Init';
break;
case consts.NET_STATE_DISCONNECTED:
currentState = 'Disconnected';
break;
case consts.NET_STATE_CONNECTED:
currentState = 'Connected';
break;
default:
break;
}
return currentState;
}
/**
* Returns true if current network connection state is INIT.
* This is state means the library has been initialised but there is no
* connection made with the network yet.
*
* @returns {Boolean}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const isNetStateInit = app.isNetStateInit();
* } catch(err) {
* throw err;
* }
* };
*/
isNetStateInit() {
return this._networkState === consts.NET_STATE_INIT;
}
/**
* Returns true if current network connection state is CONNECTED.
*
* @returns {Boolean}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const isNetStateConnected = app.isNetStateConnected();
* } catch(err) {
* throw err;
* }
* };
*/
isNetStateConnected() {
return this._networkState === consts.NET_STATE_CONNECTED;
}
/**
* Returns true if current network connection state is DISCONNECTED.
*
* @returns {Boolean}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const isNetStateDisconnected = app.isNetStateDisconnected();
* } catch(err) {
* throw err;
* }
* };
*/
isNetStateDisconnected() {
return this._networkState === consts.NET_STATE_DISCONNECTED;
}
/**
* Returns the {@link AppInfo} used to initialise current app.
* @returns {AppInfo}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const appInfo = app.appInfo;
* } catch(err) {
* throw err;
* }
* };
*/
get appInfo() {
return this._appInfo;
}
/**
* Generate the log path for the provided filename.
* If the filename provided is null, it then returns
* the path of where the safe_core log file is located.
* @param {String} [logFilename] optional log filename to generate the path
*
* @returns {Promise<String>}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const logPath = await app.logPath();
* } catch(err) {
* throw err;
* }
* };
*/
logPath(logFilename) { // eslint-disable-line class-methods-use-this
const filename = logFilename;
if (!logFilename) {
return Promise.resolve(SAFEApp.logFilePath);
}
return lib.app_output_log_path(filename);
}
/**
* @typedef {Object} AccountInfo
* Holds the information about the account.
* @property {Number} mutations_done - number of mutations performed
* with this account
* @property {Number} mutations_available - number of remaining mutations
* allowed for this account
*/
/**
* Returns account information, specifically, number of mutations done and available.
*
* @returns {Promise<AccountInfo>}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const getAccountInfo = await app.getAccountInfo();
* } catch(err) {
* throw err;
* }
* };
*/
getAccountInfo() {
return lib.app_account_info(this.connection);
}
/**
* @private
* Create a {@link SAFEApp} and try to login it through the `authUri`
* @param {AppInfo} appInfo - the AppInfo
* @param {String} authUri - URI containing the authentication info
* @param {Function} [networkStateCallBack=null] optional callback function
* to receive network state updates
* @param {InitOptions} initialisation options
* @returns {Promise<SAFEApp>} Authenticated {@link SAFEApp}
*/
static async fromAuthUri(appInfo, authUri, networkStateCallBack, options) {
const app = autoref(new SAFEApp(appInfo, networkStateCallBack, options));
await app.init();
return app.auth.loginFromUri(authUri);
}
/**
* Returns the name of the app's own container.
*
* @returns {Promise<String>}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const rootContainerName = await app.getOwnContainerName();
* } catch(err) {
* throw err;
* }
* };
*/
getOwnContainerName() {
return lib.app_container_name(this.appInfo.id);
}
/**
* @private
* Called from the native library whenever the network state
* changes.
*/
_networkStateUpdated(userData, newState) {
const prevState = this.networkState;
this.networkState = newState;
this.emit('network-state-updated', this.networkState, prevState);
this.emit(`network-state-${this.networkState}`, prevState);
if (this._networkStateCallBack) {
this._networkStateCallBack.apply(this._networkStateCallBack, [this.networkState]);
}
}
/**
* Reconnect to the network.
* Must be invoked when the client decides to connect back after the connection was lost.
* @returns {Promise}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* await app.reconnect();
* } catch(err) {
* throw err;
* }
* };
*/
reconnect() {
return lib.app_reconnect(this);
}
/**
* @private
* free the app. used by the autoref feature
* @param {SAFEApp} app - the app to free
*/
static free(app) {
// we are freed last, anything you do after this
// will probably fail.
lib.app_free(app.connection);
}
/**
* Resets the object cache kept by the underlying library.
* @returns {Promise}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* await app.clearObjectCache();
* } catch(err) {
* throw err;
* }
* };
*/
clearObjectCache() {
return lib.app_reset_object_cache(this.connection);
}
/**
* Returns true if the underlyging library was compiled against mock routing.
* @returns {Boolean}
* @example
* const safe = require( '@maidsafe/safe-node-app' );
*
* const appInfo = {
* id : 'net.maidsafe.example',
* name : 'Example SAFE App',
* vendor : 'MaidSafe.net Ltd'
* };
*
* const asyncFn = async () => {
* try {
* const app = await safe.initialiseApp(appInfo);
* const isMock = await app.appIsMock();
* } catch(err) {
* throw err;
* }
* };
*/
appIsMock() { // eslint-disable-line class-methods-use-this
return lib.app_is_mock();
}
}
SAFEApp.logFilename = null;
module.exports = SAFEApp;