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.

391 lines (338 loc) 14.4 kB
import { Calculator } from "@graperank/calculator"; import { Interpreter } from "@graperank/interpreter"; import {GrapevineData, GrapevineKeys, userId, WorldviewOutput, WorldviewKeys, GraperankSettings, Scorecards, ProtocolRequest, protocol, InterpreterProtocolStatus, CalculatorIterationStatus, WorldviewData, DEFAULT_CONTEXT, GraperankListener, GraperankNotification, sessionid, context, timestamp, ScorecardsOutput, WorldviewSettings, ScorecardsEntry, StorageProcessor } from "@graperank/util/types"; import { Protocols } from "@graperank/interpreter/protocols"; export class GrapeRankEngine { private generator: GrapeRankGenerator; private listeners: Map<sessionid, GraperankListener> = new Map(); constructor( readonly observer: userId, readonly storage: StorageProcessor, readonly protocols : Protocols ) {} async contexts() : Promise<string[]> { return (await this.storage.worldview.list({observer:this.observer})).list } // returns worldview and keys from specified or default context async worldview(context?: context, update : boolean | WorldviewSettings = false) : Promise<WorldviewOutput | undefined> { let keys : Required<WorldviewKeys> = { observer : this.observer, context : context || DEFAULT_CONTEXT } console.log('GrapeRankEngine : worldview() : keys : ', keys); let worldview : WorldviewData 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 == 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?: context, timestamp : timestamp = 0) : Promise<ScorecardsOutput | undefined>{ context = context || 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 : Required<GrapevineKeys> = { observer : this.observer, context, timestamp } let scorecards = await this.storage.scorecards.get(keys) if( scorecards) return {keys, scorecards} } return undefined } async generate(context?: context, settings? : Partial<GraperankSettings>) : Promise<WorldviewOutput | undefined> { 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 : GraperankNotification ){ this.listeners.forEach((listener,sessionid) => { try{ listener(notification) }catch(e){ this.listeners.delete(sessionid) } }) } listen(sesionid : sessionid, callback : GraperankListener | false){ if(!callback) return this.listeners.delete(sesionid) this.listeners.set(sesionid,callback) if(this.listeners.get(sesionid)) return true return false } async stopCalculating(context?: 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 } private async clearCalculating(context?: context){ let keys : Required<WorldviewKeys> = { observer : this.observer, context : context || 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 ... private async rebuild(){ } } // generate class GrapeRankGenerator { readonly keys : Required<GrapevineKeys> private worldview : WorldviewData private grapevine : GrapevineData private calculator : Calculator | undefined private interpreter : Interpreter | undefined private scorecards : ScorecardsEntry[] | undefined private _stopping : boolean private _stopped : boolean constructor( private engine : GrapeRankEngine, input : WorldviewOutput, settings? : Partial<GraperankSettings>, ){ this.worldview = input.worldview this.keys = { observer : input.keys.observer, context : input.keys.context || 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() : GraperankSettings { return this.grapevine.graperank //{...this.worldview?.settings?.graperank, ...this._settings } } set settings(settings : Partial<GraperankSettings>){ this.grapevine.graperank = {...this.grapevine.graperank, ...settings} } // update the Map of worldview.grapevines with new grapevine data // and return sorted entries array worldview.grapevines private get grapevines() : [number, GrapevineData][] { 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 : InterpreterProtocolStatus){ 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 : CalculatorIterationStatus){ 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() : Promise<GrapevineData | false> { 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? : Scorecards | undefined) : Promise<WorldviewOutput | undefined> { 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( 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( 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 : Map<protocol,ProtocolRequest> = 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 } } private async update(message : string){ // 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 : Scorecards = []) : userId[] | undefined { const raters : userId[] = [] 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 : GraperankSettings = { 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 : WorldviewData = { // 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} } }