UNPKG

@browser-network/database

Version:

A type of distributed database built on top of the distributed browser-network

395 lines (394 loc) 18.4 kB
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "@browser-network/crypto", "./LocalDB", "./util"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; var bnc = __importStar(require("@browser-network/crypto")); var LocalDB_1 = require("./LocalDB"); var util_1 = require("./util"); var debug = (0, util_1.debugFactory)('Db'); // convenience var wrapState = function (state, address, pub, priv) { return __awaiter(void 0, void 0, void 0, function () { var wrapp, signature, wrapped; return __generator(this, function (_a) { switch (_a.label) { case 0: wrapp = { id: address, timestamp: Date.now(), state: state, publicKey: pub }; return [4 /*yield*/, bnc.sign(priv, wrapp)]; case 1: signature = _a.sent(); wrapped = Object.assign(wrapp, { signature: signature }); return [2 /*return*/, wrapped]; } }); }); }; // convenience var verifySignature = function (update) { var signature = update.signature, wrapp = __rest(update, ["signature"]); return bnc.verifySignature(wrapp, signature, update.publicKey); }; // TODO I've seen a loop before, where state-offering messages are being spammed. // I don't know how this would have happened as it seems to be hard coded to send only // once per 5 seconds. var Db = /** @class */ (function () { function Db(_a) { var secret = _a.secret, appId = _a.appId, network = _a.network; var _this = this; this._denyList = {}; this._allowList = {}; this._onChangeHandlers = []; /** * @description Get all entries from our local DB, wrapped in the DB's * WrappedState type. * * @TODO: Does it need to be wrapped? Does the user ever care about this * wrapping or should they just be able to go straight to their state? */ this.getAll = function () { return _this.localDB.getAll(); }; /** * @description Clear the local storage of everyone's items. Essentially resets the machine * to as if it's never seen the network before. If it is connected still, it will * rapidly start to repopulate. */ this.clear = function () { _this.localDB.clear(); _this.runChangeHandlers(); }; /** * @description Effectively blocks a user. Adds them to our deny list, which means we'll no longer * accept updates from them, which means we will no longer forward their updates as well. Also * removes their state from our storage. * * It's up to the developer to keep track of these (probably * within the state object that they store in this db), and repopulate this list on startup. * Calling deny with an address that's already blocked is a noop and O(1) time so don't worry about * spamming this call. */ this.deny = function (address) { if (_this._denyList[address]) { return; } _this._denyList[address] = true; _this.localDB.remove(address); }; /** * @description Unblock a user. Removes them from our deny list, at which point the DB will naturally * start to repopulate that user's state. */ this.undeny = function (address) { delete _this._denyList[address]; }; /** * @description Add a user to our allow list. Once a single user is on this list, _only users on the * allow list will be recorded in the database_. All other users will automatically be ignored. * * It's up to the developer to keep track of these (probably * within the state object that they store in this db), and repopulate this list on startup. * Calling allow with an address that's already on the list is a noop and O(1) time so don't worry about * spamming this call. */ this.allow = function (address) { if (_this._allowList[address]) { return; } _this._allowList[address] = true; }; /** * @description Remove a user from the allow list. Calling this will remove the user's state from * our storage, and that user's state will no longer be forwarded either. */ this.unallow = function (address) { delete _this._allowList[address]; _this.localDB.remove(address); }; this.onMessage = function (message) { switch (message.type) { case 'state-offering': { debug(5, 'received state-offering:', message); return _this.onStateOffering(message.data, message.address); } case 'state-request': { debug(5, 'received state-request:', message); return _this.broadcastStateUpdateByStateId(message.data); } case 'state-update': { debug(5, 'received state-update:', message); return _this.onStateUpdate(message.data); } } }; /** * We will periodically inform the network of what states we have * and how old they are. If someone else hears that we have a state * newer than what they have on record, they can send us a request for * what we have. */ this.broadcastStateOfferings = function () { var offerings = {}; for (var _i = 0, _a = _this.localDB.getAll(); _i < _a.length; _i++) { var state = _a[_i]; offerings[state.id] = state.timestamp; } _this.network.broadcast({ type: 'state-offering', appId: _this.appId, data: offerings }); }; this.runChangeHandlers = function () { _this._onChangeHandlers.forEach(function (handler) { return handler(); }); }; this.networkId = network.networkId; this.appId = appId; this.localDB = new LocalDB_1.LocalDB(appId); this.address = network.address; this.secret = secret; this.network = network; this.network.on('message', function (message) { if (message.appId !== _this.appId) return; _this.onMessage(message); }); // Here we derive the pub key from the private this.publicKey = bnc.derivePubKey(secret); setInterval(this.broadcastStateOfferings, 5000); } /** * @description This is how you write data to the network. This will put whatever * state you give it into a DB specific wrapper with your state in the `state` key. */ Db.prototype.set = function (state) { return __awaiter(this, void 0, void 0, function () { var data; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, wrapState(state, this.address, this.publicKey, this.secret)]; case 1: data = _a.sent(); this.setLocal(data); // send update into the network this.broadcastStateUpdate(data); return [2 /*return*/]; } }); }); }; /** * @description Get the state of the user whose address is passed in. */ Db.prototype.get = function (address) { return this.localDB.get(address); }; /** * @description This will fire every time we update our state. This way reactive * UIs can listen for changes and update based on the new state of the world */ Db.prototype.onChange = function (handler) { this._onChangeHandlers.push(handler); }; /** * @description clear the DB of all listeners. */ Db.prototype.removeChangeHandlers = function () { this._onChangeHandlers = []; }; /** * @description clear the DB of a specific listener. */ Db.prototype.removeChangeHandler = function (func) { var _this = this; var handlers = Array.from(this._onChangeHandlers); handlers.forEach(function (handler, i) { if (handler === func) { _this._onChangeHandlers.splice(i, 1); } }); }; Db.prototype.onStateOffering = function (offerings, sender) { var _this = this; var requestState = function (address) { _this.network.broadcast({ type: 'state-request', appId: _this.appId, destination: sender, data: address }); }; for (var remoteId in offerings) { var remoteStateTimestamp = offerings[remoteId]; var localState = this.localDB.get(remoteId); if (this.isForbidden(remoteId)) { continue; } if (!localState) { // we don't even have this state yet requestState(remoteId); } else if (localState.timestamp < remoteStateTimestamp) { // This means they have a newer offering, for which we will now ask requestState(remoteId); } // Otherwise we have a newer or equal version } }; Db.prototype.onStateUpdate = function (update) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (this.isForbidden(update.publicKey)) { return [2 /*return*/, debug(5, 'state update from pubKey not allowed:', update.publicKey)]; } debug(5, 'received update from another node:', update); return [4 /*yield*/, this.verify(update)]; case 1: if (!(_a.sent())) { return [2 /*return*/]; } this.setLocal(update); return [2 /*return*/]; } }); }); }; // We won't accept state if: // * The sender is on our deny list, or // * We have an allow list going and the sender is not on it Db.prototype.isForbidden = function (address) { var isForbidden = this._denyList[address] || (Object.keys(this._allowList).length > 0 && !this._allowList[address]); return isForbidden; }; // Broadcast an update for a specific state id Db.prototype.broadcastStateUpdate = function (data) { this.network.broadcast({ type: 'state-update', appId: this.appId, data: data }); }; Db.prototype.broadcastStateUpdateByStateId = function (stateId) { var data = this.localDB.get(stateId); if (!data) { return; } this.broadcastStateUpdate(data); }; Db.prototype.setLocal = function (wrapped) { this.localDB.set(wrapped, wrapped.id); // Every time we set local, we've updated our storage, and we // want to inform the user as such this.runChangeHandlers(); }; Db.prototype.verify = function (update) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: // 1. verify timestamp is newer than last if (!this.isNew(update)) { return [2 /*return*/, false]; } return [4 /*yield*/, verifySignature(update)]; case 1: // 2. check veracity of signature if (!(_a.sent())) { debug(1, 'update does not pass verification! update, local version:', update, this.localDB.get(update.id)); // TODO add motrucka to rude list return [2 /*return*/, false]; } return [2 /*return*/, true]; } }); }); }; Db.prototype.isNew = function (update) { var local = this.get(update.id); if (!local) return true; return update.timestamp > local.timestamp; }; return Db; }()); exports["default"] = Db; });