@x5e/gink
Version:
an eventually consistent database
140 lines (132 loc) • 4.85 kB
text/typescript
import { LogBackedStore } from "./LogBackedStore";
import { Store } from "./Store";
import { Database } from "./Database";
import { AuthFunction } from "./typedefs";
import { SimpleServer } from "./SimpleServer";
import {
ensure,
generateTimestamp,
getIdentity,
logToStdErr,
noOp,
isAlive,
} from "./utils";
import { IndexedDbStore } from "./IndexedDbStore";
import { start, REPLServer } from "node:repl";
import { Directory } from "./Directory";
import { Box } from "./Box";
import { Sequence } from "./Sequence";
import { KeySet } from "./KeySet";
import { Accumulator } from "./Accumulator";
/**
Intended to manage server side running of Gink.
Basically it takes some settings in the form of
arguments plus a list of peers to
connect to then starts up the Gink Instance,
or Gink Server if port listening is specified.
*/
export class CommandLineInterface {
targets: string[];
store?: Store;
instance?: Database;
replServer?: REPLServer;
authToken?: string;
reconnectOnClose: boolean;
logger: (_: string) => void;
constructor(args: {
connect_to?: string[];
listen_on?: string;
data_file?: string;
identity?: string;
reconnect?: boolean;
static_path?: string;
auth_token?: string;
ssl_cert?: string;
ssl_key?: string;
verbose?: boolean;
exclusive?: boolean;
}) {
// This makes debugging through integration tests way easier.
globalThis.ensure = ensure;
this.logger = args.verbose ? logToStdErr : noOp;
this.authToken = args.auth_token;
this.reconnectOnClose = args.reconnect;
this.targets = args.connect_to ?? [];
const identity = args.identity ?? getIdentity();
ensure(identity);
let authFunc: AuthFunction | null = null;
if (this.authToken) {
authFunc = (token: string) => {
// Expecting token to have already been decoded from hex.
if (!token) return false;
// Purposely using includes here since the token will have been
// decoded and may contain '\x00' as a prefix.
ensure(token.includes("token "));
let key = this.authToken.toLowerCase();
token = token.toLowerCase().split("token ")[1].trimStart();
return token === key;
};
}
if (args.data_file) {
const is_exclusive = args.exclusive ? "exclusive" : "not exclusive";
this.logger(`using data file=${args.data_file} (${is_exclusive})`);
this.store = new LogBackedStore(args.data_file, args.exclusive);
} else {
this.logger(`using in-memory database`);
this.store = new IndexedDbStore(generateTimestamp().toString());
}
if (args.listen_on) {
const common = {
port: args.listen_on,
sslKeyFilePath: args.ssl_key,
sslCertFilePath: args.ssl_cert,
staticContentRoot: args.static_path,
logger: this.logger,
authFunc: authFunc,
};
this.instance = new SimpleServer({
store: this.store,
identity: identity,
...common,
});
} else {
// port not set so don't listen for incoming connections
this.instance = new Database({
store: this.store,
identity,
logger: this.logger,
});
}
}
async run() {
if (this.instance) {
await this.instance.ready;
globalThis.isAlive = isAlive;
globalThis.database = this.instance;
globalThis.root = Directory.get(this.instance);
globalThis.Accumulator = Accumulator;
globalThis.Sequence = Sequence;
globalThis.Box = Box;
globalThis.KeySet = KeySet;
globalThis.Directory = Directory;
for (const target of this.targets) {
this.logger(`connecting to: ${target}`);
await this.instance.connectTo(target, {
reconnectOnClose: this.reconnectOnClose,
authToken: this.authToken,
onError: (e) => {
this.logger(
`Failed connection to ${target}. Bad Auth token?\n` +
e,
);
},
}).ready;
this.logger(`connected!`);
}
}
this.replServer = start({ prompt: "node+gink> ", useGlobal: true });
this.replServer.on("exit", () => {
process.exit(0);
});
}
}