wranglebot
Version:
open source media asset management
439 lines • 18.6 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import Transaction from "./Transaction.js";
import LogBot from "logbotjs";
import { io } from "socket.io-client";
import { config, finder } from "../system/index.js";
import EventEmitter from "events";
import { clearTimeout } from "timers";
import md5 from "md5";
import { v4 as uuidv4 } from "uuid";
class DB extends EventEmitter {
constructor(options) {
super();
this.readOnly = false;
this.key = undefined;
this.transactions = [];
this.localModal = {};
this.offline = true;
this.commitIntervalPause = 5000;
this.unsavedChanges = false;
this.saving = false;
this.keySalt = "Wr4ngle_b0t";
if (!options.url && !options.token)
throw new Error("No database or token provided. Aborting.");
this.url = options.url;
this.token = options.token;
this.pathToTransactions = config.getPathToUserData() + "/transactions/" + `${md5(this.token + this.keySalt)}`;
if (!finder.existsSync(this.pathToTransactions)) {
finder.mkdirSync(this.pathToTransactions, { recursive: true });
}
}
rebuildLocalModel() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.url && this.token) {
let skip = false;
if (!finder.existsSync(this.pathToTransactions)) {
finder.mkdirSync(this.pathToTransactions, { recursive: true });
skip = true;
}
if (finder.getContentOfFolder(this.pathToTransactions).length === 0) {
yield this.saveTransaction(new Transaction({
$collection: "users",
$query: {
id: uuidv4(),
},
$set: {
username: "admin",
password: md5("admin" + this.keySalt),
roles: ["admin"],
firstName: "Admin",
lastName: "Admin",
email: "admin@wranglebot.local",
},
$method: "updateOne",
}));
}
}
if (finder.exists("transactions")) {
let t = Date.now();
let folderContents = finder.getContentOfFolder(this.pathToTransactions);
const transactions = [];
for (let file of folderContents) {
try {
const parsedData = JSON.parse(finder.parseFileSync(finder.join(this.pathToTransactions, file)));
transactions.push(new Transaction(parsedData));
}
catch (e) {
LogBot.log(500, "Could not parse transaction file " + file + ". Ignoring File.");
}
}
transactions.sort((a, b) => {
return a.timestamp - b.timestamp > 0 ? 1 : -1;
});
this.localModal = {};
let transactionCounts = {
committed: 0,
rejected: 0,
pending: 0,
};
for (let transaction of transactions) {
this.transactions.push(transaction);
this.apply(transaction);
transactionCounts[transaction.getStatus()]++;
}
LogBot.log(200, "Parsed " + transactions.length + " transactions in " + (Date.now() - t) + "ms");
if (transactionCounts.rejected > 0)
LogBot.log(200, "Transactions Rejected: " + transactionCounts.rejected);
if (transactionCounts.pending > 0)
LogBot.log(200, "Transactions Pending: " + transactionCounts.pending);
}
});
}
connect() {
return __awaiter(this, arguments, void 0, function* (token = this.token) {
clearTimeout(this.connectTimeout);
if (!token)
throw new Error("No token provided. Aborting.");
this.token = token;
try {
if (!this.socket)
yield this.listen();
if (!this.socket.connected)
throw new Error("Socket not connected");
}
catch (e) {
LogBot.log(600, "Could not connect to database. Trying again in 5 seconds.");
this.connectTimeout = setTimeout(() => {
this.connect(token);
}, 5000);
}
return this;
});
}
fetchTransactions() {
return new Promise((resolve) => {
this.socket.emit("fetchTransactions", this.transactions.map((t) => t.uuid));
this.socket.once("sync-start", (syncInfo) => {
LogBot.log(100, "Syncing " + syncInfo.totalTransactions + " transactions");
this.$emit("notification", {
title: "Syncing",
message: "Syncing " + syncInfo.totalTransactions + " transactions",
});
let syncedTransactions = 0;
let blockIndex = 0;
if (syncInfo.status === "synced") {
LogBot.log(200, "Already synced. Skipping.");
this.$emit("notification", {
title: "Synced",
message: "Already synced. Skipping.",
});
resolve(true);
return;
}
this.socket.on("sync-block", (transactions) => {
for (let transaction of transactions) {
LogBot.log(200, "Received transaction " + transaction.uuid + " from server (" + syncedTransactions + "/" + syncInfo.totalTransactions + ")");
this.$emit("notification", {
title: "Syncing",
message: "Transaction ... " + syncedTransactions + "/" + syncInfo.totalTransactions,
});
this.addTransactionToQueue(new Transaction({
$collection: transaction.$collection,
$method: transaction.$method,
$query: transaction.$query,
$set: transaction.$set,
timestamp: transaction.timestamp,
uuid: transaction.uuid,
status: "success",
}), true);
syncedTransactions++;
}
this.socket.emit("sync-block-ack:" + blockIndex, { status: "success" });
blockIndex++;
});
this.socket.once("sync-end", () => {
LogBot.log(200, "Finished Syncing " + syncedTransactions + " transactions");
this.$emit("notification", {
title: "Synced",
message: "Finished Syncing " + syncedTransactions + " transactions",
});
resolve(true);
});
});
});
}
$emit(event, ...args) {
this.emit(event, ...args);
}
apply(transaction) {
if (!transaction.isRejected()) {
let collection = this.localModal[transaction.$collection];
if (!collection) {
this.localModal[transaction.$collection] = [];
collection = this.localModal[transaction.$collection];
}
const index = collection.findIndex((c) => {
for (let key in transaction.$query) {
if (c[key] !== transaction.$query[key])
return false;
}
return true;
});
if (transaction.$method === "updateOne") {
if (index !== -1) {
for (let key in transaction.$set) {
if (typeof transaction.$set[key] === "object" && !Array.isArray(transaction.$set[key]) && transaction.$set[key] !== null) {
let keyContent = transaction.$set[key];
if (!collection[index][key]) {
collection[index][key] = {};
}
for (let subKey in keyContent) {
collection[index][key][subKey] = keyContent[subKey];
}
}
else {
collection[index][key] = JSON.parse(JSON.stringify(transaction.$set[key]));
}
}
}
else {
collection.push(JSON.parse(JSON.stringify(Object.assign(Object.assign({}, transaction.$query), transaction.$set))));
}
}
else if (transaction.$method === "insertMany") {
if (transaction.$set instanceof Array) {
for (let doc of transaction.$set) {
collection.push(JSON.parse(JSON.stringify(doc)));
}
}
else {
collection.push(JSON.parse(JSON.stringify(transaction.$set)));
}
}
else if (transaction.$method === "removeOne") {
if (index !== -1) {
collection.splice(index, 1);
}
else {
}
}
else if (transaction.$method === "removeMany") {
for (let i = 0; i < collection.length; i++) {
const doc = collection[i];
let match = true;
for (let key in transaction.$query) {
if (doc[key] !== transaction.$query[key]) {
match = false;
break;
}
}
if (match) {
collection.splice(i, 1);
i--;
}
}
}
}
else {
throw new Error("Transaction corrupted. Aborting. Please delete transactions file and resync from server.");
}
}
getTransactions(filter) {
let transactions = this.transactions;
for (let key in filter) {
transactions = transactions.filter((t) => t.$query[key] === filter[key] || t.$set[key] === filter[key]);
}
return transactions;
}
addTransaction(method, collection, query, set, save = true) {
if (this.readOnly)
throw new Error("Database is in read-only mode. Aborting.");
const transaction = new Transaction({
$method: method,
$collection: collection,
$query: query,
$set: set,
});
this.addTransactionToQueue(transaction, save);
return transaction;
}
addTransactionToQueue(transaction, save = true) {
const existingTransaction = this.transactions.find((t) => t.uuid === transaction.uuid);
if (!existingTransaction) {
let timestamp = transaction.timestamp;
let index = this.transactions.findIndex((t) => t.timestamp > timestamp);
if (index === -1) {
index = this.transactions.length;
this.transactions.splice(index, 0, transaction);
this.apply(transaction);
}
else {
this.transactions.splice(index, 0, transaction);
for (let i = index; i < this.transactions.length; i++) {
this.apply(this.transactions[i]);
}
}
if (save)
this.saveTransaction(transaction);
return true;
}
else {
LogBot.log(409, "Transaction already exists in ledger");
return false;
}
}
commit() {
return __awaiter(this, void 0, void 0, function* () {
if (this.readOnly)
throw new Error("Database is in read-only mode. Aborting.");
if (this.offline)
return;
const toCommit = this.transactions.filter((t) => !t.isCommitted());
for (let transaction of toCommit) {
try {
yield transaction.$commit(this.socket);
this.saveTransaction(transaction);
}
catch (e) {
LogBot.log(500, e.message);
}
}
if (toCommit.length > 0) {
LogBot.log(200, "Committed a total of " +
toCommit.length +
" transactions. Successful: " +
toCommit.filter((t) => t.isCommitted()).length +
" Rejected: " +
toCommit.filter((t) => t.isRejected()).length);
return toCommit.length === toCommit.filter((t) => t.isCommitted()).length;
}
else {
return true;
}
});
}
listen() {
return new Promise((resolve, reject) => {
if (!this.url)
reject(new Error("No url provided, can not connect to a cloud node"));
this.socket = io(this.url, {
reconnectionDelayMax: 5000,
reconnection: true,
reconnectionAttempts: Infinity,
auth: {
token: this.token,
},
});
let timer = setTimeout(() => {
if (!this.socket.connected) {
this.offline = true;
reject(new Error("Could not connect to database"));
}
}, 5000);
const commitIntervalFunc = () => {
this.commit().then(() => {
setTimeout(commitIntervalFunc, this.commitIntervalPause);
});
};
this.socket.on("transaction", (data) => {
const t = new Transaction(Object.assign(Object.assign({}, data), { status: "success" }));
LogBot.log(100, `Received transaction ${t.uuid} from peer`);
if (this.addTransactionToQueue(t, true)) {
this.emit("transaction", t);
}
});
this.socket.on("disconnect", () => {
if (!this.offline) {
this.offline = true;
clearTimeout(this.commitInterval);
LogBot.log(408, "Disconnected from peer");
}
});
this.socket.on("connect", () => {
clearTimeout(timer);
this.offline = false;
LogBot.log(200, "Connected to peer");
this.offline = false;
this.fetchTransactions().then(() => {
this.commit().then(() => {
this.commitInterval = setTimeout(commitIntervalFunc, this.commitIntervalPause);
resolve(true);
});
});
});
});
}
getOne(collection, query) {
if (!this.localModal[collection])
return null;
const collectionData = this.localModal[collection].find((c) => {
for (let key in query) {
if (c[key] !== query[key])
return false;
}
return true;
});
return JSON.parse(JSON.stringify(collectionData));
}
getMany(collection, query) {
if (!this.localModal[collection])
return [];
const collectionData = this.localModal[collection].filter((c) => {
for (let key in query) {
if (c[key] !== query[key])
return false;
}
return true;
});
return JSON.parse(JSON.stringify(collectionData));
}
updateOne(collection, query, set, save = true) {
return this.addTransaction("updateOne", collection, JSON.parse(JSON.stringify(query)), JSON.parse(JSON.stringify(set)), save);
}
removeOne(collection, query, save = true) {
return this.addTransaction("removeOne", collection, JSON.parse(JSON.stringify(query)), {}, save);
}
removeMany(collection, query, save = true) {
return this.addTransaction("removeMany", collection, JSON.parse(JSON.stringify(query)), {}, save);
}
insertMany(collection, query, data, save = true) {
return this.addTransaction("insertMany", collection, query, JSON.parse(JSON.stringify(data)), save);
}
saveTransaction(transaction) {
return new Promise((resolve, reject) => {
finder
.saveAsync(`/transactions/${md5(this.token + this.keySalt)}/${transaction.uuid}`, JSON.stringify(transaction))
.then(() => {
LogBot.log(200, "Saved transaction " + transaction.uuid + " to disk");
resolve(true);
})
.catch((e) => {
reject(e);
});
});
}
}
let db;
const getDB = (options = undefined) => {
if (db instanceof DB)
return db;
else if (options) {
db = new DB({
url: options.url,
token: options.token,
});
return db;
}
throw new Error("No database instance found");
};
export default getDB;
export { DB };
//# sourceMappingURL=DB.js.map