204 lines
6.0 KiB
JavaScript
204 lines
6.0 KiB
JavaScript
"use strict";
|
|
|
|
// This module exports a function that connects to SSB and returns an interface
|
|
// to call methods over MuxRPC. It's a thin wrapper around SSB-Client, which is
|
|
// a thin wrapper around the MuxRPC module.
|
|
|
|
const { promisify } = require("util");
|
|
const ssbClient = require("ssb-client");
|
|
const ssbConfig = require("ssb-config");
|
|
const ssbTangle = require("ssb-tangle");
|
|
const debug = require("debug")("oasis");
|
|
const path = require("path");
|
|
const lodash = require("lodash");
|
|
|
|
const flotilla = require("./flotilla");
|
|
|
|
const socketPath = path.join(ssbConfig.path, "socket");
|
|
const publicInteger = ssbConfig.keys.public.replace(".ed25519", "");
|
|
const remote = `unix:${socketPath}~noauth:${publicInteger}`;
|
|
|
|
/**
|
|
* @param formatter {string} input
|
|
* @param args {any[]} input
|
|
*/
|
|
const log = (formatter, ...args) => {
|
|
const isDebugEnabled = debug.enabled;
|
|
debug.enabled = true;
|
|
debug(formatter, ...args);
|
|
debug.enabled = isDebugEnabled;
|
|
};
|
|
|
|
/**
|
|
* @param [options] {object} - options to pass to SSB-Client
|
|
* @returns Promise
|
|
*/
|
|
const connect = (options) =>
|
|
new Promise((resolve, reject) => {
|
|
const onSuccess = (api) => {
|
|
if (api.tangle === undefined) {
|
|
// HACK: SSB-Tangle isn't available in Patchwork, but we want that
|
|
// compatibility. This code automatically injects SSB-Tangle into our
|
|
// stack so that we don't get weird errors when using Patchwork.
|
|
//
|
|
// See: https://github.com/fraction/oasis/issues/21
|
|
api.tangle = ssbTangle.init(api);
|
|
|
|
// MuxRPC supports promises but the raw plugin does not.
|
|
api.tangle.branch = promisify(api.tangle.branch);
|
|
}
|
|
|
|
resolve(api);
|
|
};
|
|
|
|
ssbClient(null, options).then(onSuccess).catch(reject);
|
|
});
|
|
|
|
let closing = false;
|
|
let serverHandle;
|
|
let clientHandle;
|
|
|
|
/**
|
|
* Attempts connection over Unix socket, falling back to TCP socket if that
|
|
* fails. If the TCP socket fails, the promise is rejected.
|
|
* @returns Promise
|
|
*/
|
|
const attemptConnection = () =>
|
|
new Promise((resolve, reject) => {
|
|
connect({ remote })
|
|
.then((ssb) => {
|
|
debug("Connected to existing Scuttlebutt service over Unix socket");
|
|
resolve(ssb);
|
|
})
|
|
.catch((e) => {
|
|
if (closing) return;
|
|
debug("Unix socket failed");
|
|
if (e.message !== "could not connect to sbot") {
|
|
throw e;
|
|
}
|
|
connect()
|
|
.then((ssb) => {
|
|
log("Connected to existing Scuttlebutt service over TCP socket");
|
|
resolve(ssb);
|
|
})
|
|
.catch((e) => {
|
|
if (closing) return;
|
|
debug("TCP socket failed");
|
|
if (e.message !== "could not connect to sbot") {
|
|
throw e;
|
|
}
|
|
reject(new Error("Both connection options failed"));
|
|
});
|
|
});
|
|
});
|
|
|
|
let pendingConnection = null;
|
|
|
|
const ensureConnection = (customConfig) => {
|
|
if (pendingConnection === null) {
|
|
pendingConnection = new Promise((resolve) => {
|
|
attemptConnection()
|
|
.then((ssb) => {
|
|
resolve(ssb);
|
|
})
|
|
.catch(() => {
|
|
debug("Connection attempts to existing Scuttlebutt services failed");
|
|
log("Starting Scuttlebutt service");
|
|
|
|
// Adjust with `customConfig`, which declares further preferences.
|
|
serverHandle = flotilla(customConfig);
|
|
|
|
// Give the server a moment to start. This is a race condition. :/
|
|
setTimeout(() => {
|
|
attemptConnection()
|
|
.then(resolve)
|
|
.catch((e) => {
|
|
throw new Error(e);
|
|
});
|
|
}, 100);
|
|
});
|
|
});
|
|
|
|
const cancel = () => (pendingConnection = null);
|
|
pendingConnection.then(cancel, cancel);
|
|
}
|
|
|
|
return pendingConnection;
|
|
};
|
|
|
|
module.exports = ({ offline }) => {
|
|
if (offline) {
|
|
log("Offline mode activated - not connecting to scuttlebutt peers or pubs");
|
|
log(
|
|
"WARNING: Oasis can connect to the internet through your other SSB apps if they're running."
|
|
);
|
|
}
|
|
|
|
// Make a copy of `ssbConfig` to avoid mutating.
|
|
const customConfig = JSON.parse(JSON.stringify(ssbConfig));
|
|
|
|
// This is unnecessary when https://github.com/ssbc/ssb-config/pull/72 is merged
|
|
customConfig.connections.incoming.unix = [
|
|
{ scope: "device", transform: "noauth" },
|
|
];
|
|
|
|
// Only change the config if `--offline` is true.
|
|
if (offline === true) {
|
|
lodash.set(customConfig, "conn.autostart", false);
|
|
}
|
|
|
|
// Use `conn.hops`, or default to `friends.hops`, or default to `0`.
|
|
lodash.set(
|
|
customConfig,
|
|
"conn.hops",
|
|
lodash.get(ssbConfig, "conn.hops", lodash.get(ssbConfig.friends.hops, 0))
|
|
);
|
|
|
|
/**
|
|
* This is "cooler", a tiny interface for opening or reusing an instance of
|
|
* SSB-Client.
|
|
*/
|
|
const cooler = {
|
|
open() {
|
|
// This has interesting behavior that may be unexpected.
|
|
//
|
|
// If `clientHandle` is already an active [non-closed] connection, return that.
|
|
//
|
|
// If the connection is closed, we need to restart it. It's important to
|
|
// note that if we're depending on an external service (like Patchwork) and
|
|
// that app is closed, then Oasis will seamlessly start its own SSB service.
|
|
return new Promise((resolve, reject) => {
|
|
if (clientHandle && clientHandle.closed === false) {
|
|
resolve(clientHandle);
|
|
} else {
|
|
ensureConnection(customConfig).then((ssb) => {
|
|
clientHandle = ssb;
|
|
if (closing) {
|
|
cooler.close();
|
|
reject(new Error("Closing Oasis"));
|
|
} else {
|
|
resolve(ssb);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
close() {
|
|
closing = true;
|
|
if (clientHandle && clientHandle.closed === false) {
|
|
clientHandle.close();
|
|
}
|
|
if (serverHandle) {
|
|
serverHandle.close();
|
|
}
|
|
},
|
|
};
|
|
|
|
// Important: This ensures that we have an SSB connection as soon as Oasis
|
|
// starts. If we don't do this, then we don't even attempt an SSB connection
|
|
// until we receive our first HTTP request.
|
|
cooler.open();
|
|
|
|
return cooler;
|
|
};
|