UNPKG

@graperank/engine

Version:

The GrapeRank Engine module stores and retrieves generated scores, and runs the Calculator and Interpretor modules to generate new scores. It requires one storage and one or more protocol plugins as input.

349 lines (348 loc) 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GrapeRankEngine = void 0; const calculator_1 = require("@graperank/calculator"); const interpreter_1 = require("@graperank/interpreter"); const types_1 = require("@graperank/util/types"); class GrapeRankEngine { observer; storage; protocols; generator; listeners = new Map(); constructor(observer, storage, protocols) { this.observer = observer; this.storage = storage; this.protocols = protocols; } async contexts() { return (await this.storage.worldview.list({ observer: this.observer })).list; } // returns worldview and keys from specified or default context async worldview(context, update = false) { let keys = { observer: this.observer, context: context || types_1.DEFAULT_CONTEXT }; console.log('GrapeRankEngine : worldview() : keys : ', keys); let worldview; if (!update) { // retrieve worldview from storage worldview = await this.storage.worldview.get(keys); console.log('GrapeRankEngine : worldview() : retrieved worldview : ', worldview); // OR create first worldview ... if DEFAULT does not exist if (!worldview) { // MUST deconstruct the WORLDVIEW_DEFAULT object worldview = keys.context == types_1.DEFAULT_CONTEXT ? { ...WORLDVIEW_DEFAULT } : undefined; console.log('GrapeRankEngine : worldview() : default worldview : ', worldview); } } else { // create a new worldview with possibly custom settings // MUST deconstruct the WORLDVIEW_DEFAULT object worldview = update === true ? { ...WORLDVIEW_DEFAULT } : { settings: update }; console.log('GrapeRankEngine : worldview() : new worldview : ', worldview); } if (!worldview) { console.log('GrapeRankEngine : worldview() : no worldview found or created'); return undefined; } console.log('GrapeRankEngine : worldview() : returning worldview : ', { keys, worldview }); return { keys, worldview }; } // get calculated scorecards async scorecards(context, timestamp = 0) { context = context || types_1.DEFAULT_CONTEXT; if (!timestamp) { let { worldview } = await this.worldview(context); // get timestamp from `worldview.calculated` timestamp = worldview.calculated; // get timestamp from last `worldview.grapevines` if (!timestamp && worldview.grapevines.length) { timestamp = worldview.grapevines[worldview.grapevines.length - 1][0]; } } if (timestamp) { let keys = { observer: this.observer, context, timestamp }; let scorecards = await this.storage.scorecards.get(keys); if (scorecards) return { keys, scorecards }; } return undefined; } async generate(context, settings) { let input = await this.worldview(context, true); let stopped = this.generator ? await this.stopCalculating(input.keys.context) : true; if (!stopped) return undefined; this.generator = new GrapeRankGenerator(this, input, settings); return await this.generator.generate(); } // handles notification for listeners across all instances of GrapeRank notify(notification) { this.listeners.forEach((listener, sessionid) => { try { listener(notification); } catch (e) { this.listeners.delete(sessionid); } }); } listen(sesionid, callback) { if (!callback) return this.listeners.delete(sesionid); this.listeners.set(sesionid, callback); if (this.listeners.get(sesionid)) return true; return false; } async stopCalculating(context) { let stopped = this.generator ? await this.generator.stop() : true; console.log('GrapeRank : stopCalculating() : generator stopped : ', stopped); let cleared = stopped ? await this.clearCalculating(context) : false; console.log('GrapeRank : stopCalculating() : worldview calculating cleared : ', cleared); return cleared; } async clearCalculating(context) { let keys = { observer: this.observer, context: context || types_1.DEFAULT_CONTEXT }; let worldview = await this.storage.worldview.get(keys); if (!worldview?.calculating) return true; worldview.grapevines.pop(); worldview.calculating = undefined; return await this.storage.worldview.put(keys, worldview); } // TODO rebuild worldviews from existing (stored) scorecards // IF worldview data was accidentally obliterated or lost ... async rebuild() { } } exports.GrapeRankEngine = GrapeRankEngine; // generate class GrapeRankGenerator { engine; keys; worldview; grapevine; calculator; interpreter; scorecards; _stopping; _stopped; constructor(engine, input, settings) { this.engine = engine; this.worldview = input.worldview; this.keys = { observer: input.keys.observer, context: input.keys.context || types_1.DEFAULT_CONTEXT, timestamp: 0 }; this.grapevine = { graperank: { ...GRAPERANK_DEFAULT, ...input.worldview.settings.graperank, ...settings }, status: { completed: 0, total: 0, interpreter: [], calculator: [] } }; } // get ratings() { return this.interpretations.ratings } get completed() { return this.grapevine.status.completed; } get settings() { return this.grapevine.graperank; //{...this.worldview?.settings?.graperank, ...this._settings } } set settings(settings) { this.grapevine.graperank = { ...this.grapevine.graperank, ...settings }; } // update the Map of worldview.grapevines with new grapevine data // and return sorted entries array worldview.grapevines get grapevines() { let grapevines = new Map(this.worldview.grapevines); grapevines.set(this.keys.timestamp, this.grapevine); // sort newest timestamp is last in list return [...grapevines].sort((a, b) => a[0] - b[0]); } // async initWorldview() : Promise<WorldviewData> { // this.worldview = this.worldview || this.keys.context ? (await this.engine.worldview(this.keys.context)).worldview : WORLDVIEW_DEFAULT // return this.worldview // } async updateInterpreterStatus(newstatus) { let updated = false; this.grapevine.status.interpreter.forEach((status) => { if (!updated && status.protocol == newstatus.protocol && status.dos == newstatus.dos) { status = newstatus; updated = true; } }); if (!updated) this.grapevine.status.interpreter.push(newstatus); return await this.update('Interpretation updated for ' + newstatus.protocol + newstatus.dos ? '[' + newstatus.dos + ']' : ''); } async updateCalculatorStatus(newstatus) { let iteration = this.grapevine.status.calculator.length; this.grapevine.status.calculator.push(newstatus); return await this.update('Calculator iteration ' + iteration + ' complete'); } // TODO retrieving final grapevine should finalize the status object async updateCalculatorComplete() { this.grapevine.status.completed = Date.now() - this.keys.timestamp; let finaliteration = this.grapevine.status.calculator[this.grapevine.status.calculator.length - 1]; for (let dos in finaliteration) { this.grapevine.status.total += ((finaliteration[dos].calculated || 0) + (finaliteration[dos].uncalculated || 0)); } if (await this.update('Calculation complete')) return this.grapevine; return false; } // private calculate = Calculator.calculate // private interpret = Interpreter.interpret async generate(scorecards) { try { this.keys.timestamp = Date.now(); // set worldcview.calculating this.worldview.calculating = this.keys.timestamp; await this.update('Generating a new grapevine for : ' + this.keys.context); // Interpret ratings const raters = getRaters(scorecards) || [this.keys.observer]; console.log("GrapeRank : calling interpret with ", raters.length, " authors ..."); // initiate the interpretation and calculation engines to run in the background // while writing status updates to the grapevine object in storage (via GrapeRankGenerator) this.interpreter = new interpreter_1.Interpreter(this.engine.protocols, this.updateInterpreterStatus.bind(this)); const interpretations = await this.interpreter.interpret(raters, this.settings.interpreters); // const ratings : RatingsList = interpeterresults.ratings if (this.stopping || !interpretations) throw ('stopping'); // Calculate scorecards console.log("GrapeRank : calling calculate with " + interpretations.ratings.length + " ratings... "); this.calculator = new calculator_1.Calculator(this.keys.observer, interpretations.ratings, this.settings.calculator, this.updateCalculatorStatus.bind(this), this.updateCalculatorComplete.bind(this)); this.scorecards = await this.calculator.calculate(); if (this.stopping || !this.scorecards) throw ('stopping'); // write to storage // Update worldview calculating & calculated if (this.grapevine.status?.completed) this.worldview.calculating = undefined; if (this.worldview.settings?.overwrite !== false) this.worldview.calculated = this.keys.timestamp; // set grapevine expires if (this.worldview.settings?.expiry) this.grapevine.expires = this.keys.timestamp + this.worldview.settings.expiry; // set graperank interpreters used for this grapevine // TODO these interpreters should be added to `status` during interpretation phase to generate live updates let interpreters = new Map(); interpretations.responses.forEach((response) => { interpreters.set(response.request.protocol, response.request); }); this.grapevine.graperank = { ...this.worldview.settings.graperank, ...this.grapevine.graperank, interpreters: [...interpreters.values()] }; // send new scorecards to storage await this.engine.storage.scorecards.put(this.keys, this.scorecards); // send worldview (and grapevines) to storage await this.update('Grapevine scorecards have been generated.'); if (this.worldview.settings?.archive === false) { // TODO delete old grapevines } return { worldview: this.worldview, keys: this.keys }; } catch (e) { this._stopped = true; console.log('GrapeRank : generator stopped.', this.keys, e); return undefined; } } async update(message) { // assure that GrapeRank worldview OR default is loaded // await this.initWorldview() if (this.stopping) throw ('stopping'); let stored = false; this.worldview.grapevines = this.grapevines; stored = await this.engine.storage.worldview.put(this.keys, this.worldview); if (stored) this.engine.notify({ message, keys: this.keys, grapevine: this.grapevine }); return true; } get stopping() { return this._stopping; } get stopped() { return this._stopped; } async stop() { if (this.worldview.calculating) { this._stopping = true; if (this.interpreter) this.interpreter.stop(); if (this.calculator) this.calculator.stop(); while (!this.stopped) { await new Promise(resolve => setTimeout(resolve, 1000)); } return this.stopped; } return true; } } function getRaters(scorecards = []) { const raters = []; scorecards.forEach((entry) => { raters.push(entry[0]); }); return raters.length ? raters : undefined; } // MUST deconstruct the GRAPERANK_DEFAULT object when used // OTHERWISE updated properties will persist across engine instances const GRAPERANK_DEFAULT = { interpreters: [ { protocol: "nostr-follows", iterate: 6 }, { protocol: "nostr-mutes", }, { protocol: "nostr-reports", } ], calculator: { // incrementally decrease influence weight attenuation: .7, // factor for calculating confidence // MUST be bellow 1 or confidence will ALWAYS be 0 // CAUTION : too high (eg:.7) and users beyond a certain DOS (eg:2) will always have a score of zero rigor: .2, // minimum score ABOVE WHICH scorecard will be included in output minscore: 0, // max difference between calculator iterations // ZERO == most precise precision: 0, // devmode if off by default devmode: false }, }; // MUST deconstruct the WORLDVIEW_DEFAULT object when used // OTHERWISE updated properties will persist across engine instances const WORLDVIEW_DEFAULT = { // timestamp of preffered grapevine calculation // calculating : undefined, // timestamp of preffered grapevine calculation // calculated : undefined, settings: { // overwrite 'calculated' timestamp when calculating new grapevine? overwrite: true, // retain historical grapevines when calculating new? archive: true, // duration for 'expires' timestamp of new grapevines from calculation time expiry: undefined, // default 'graperank' settings for new grapevine calculations graperank: { ...GRAPERANK_DEFAULT } } };