134 lines
4.5 KiB
JavaScript
134 lines
4.5 KiB
JavaScript
import { Actor } from './actor.js';
|
|
import { Graph } from './graph.js';
|
|
import { Action } from './action.js';
|
|
import { CryptoUtil } from './crypto.js';
|
|
import params from '../params.js';
|
|
|
|
class Post extends Actor {
|
|
constructor(forum, authorPublicKey, postContent) {
|
|
const index = forum.posts.countVertices();
|
|
const name = `Post${index + 1}`;
|
|
super(name, forum.scene);
|
|
this.id = postContent.id ?? CryptoUtil.randomUUID();
|
|
this.authorPublicKey = authorPublicKey;
|
|
this.value = 0;
|
|
this.citations = postContent.citations;
|
|
this.title = postContent.title;
|
|
const revaluationTotal = this.citations.reduce((total, { weight }) => total += Math.abs(weight), 0);
|
|
if (revaluationTotal > params.revaluationLimit) {
|
|
throw new Error('Post revaluation total exceeds revaluation limit '
|
|
+ `(${revaluationTotal} > ${params.revaluationLimit})`);
|
|
}
|
|
if (this.citations.some(({ weight }) => Math.abs(weight) > 1)) {
|
|
throw new Error('Each citation weight must be in the range [-1, 1]');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Purpose: Maintain a directed, acyclic, weighted graph of posts referencing other posts
|
|
*/
|
|
export class Forum extends Actor {
|
|
constructor(name, scene) {
|
|
super(name, scene);
|
|
this.posts = new Graph(scene);
|
|
this.actions = {
|
|
addPost: new Action('add post', scene),
|
|
propagateValue: new Action('propagate value', this.scene),
|
|
};
|
|
}
|
|
|
|
async addPost(authorId, postContent) {
|
|
const post = new Post(this, authorId, postContent);
|
|
await this.actions.addPost.log(this, post);
|
|
this.posts.addVertex(post.id, post, post.title);
|
|
if (this.scene.flowchart) {
|
|
this.scene.flowchart.log(`${post.id} -- value --> ${post.id}_value[0]`);
|
|
}
|
|
for (const { postId: citedPostId, weight } of post.citations) {
|
|
this.posts.addEdge('citation', post.id, citedPostId, { weight });
|
|
if (this.scene.flowchart) {
|
|
this.scene.flowchart.log(`${post.id} -- ${weight} --> ${citedPostId}`);
|
|
}
|
|
}
|
|
return post.id;
|
|
}
|
|
|
|
getPost(postId) {
|
|
return this.posts.getVertexData(postId);
|
|
}
|
|
|
|
getPosts() {
|
|
return this.posts.getVerticesData();
|
|
}
|
|
|
|
async setPostValue(post, value) {
|
|
post.value = value;
|
|
await post.setValue('value', value);
|
|
if (this.scene.flowchart) {
|
|
this.scene.flowchart.log(`${post.id}_value[${value}]`);
|
|
}
|
|
}
|
|
|
|
getTotalValue() {
|
|
return this.getPosts().reduce((total, { value }) => total += value, 0);
|
|
}
|
|
|
|
async onValidate(bench, pool, postId, initialValue) {
|
|
initialValue *= params.initialPostValue();
|
|
|
|
if (this.scene.flowchart) {
|
|
this.scene.flowchart.log(`${postId}_initial_value[${initialValue}] -- initial value --> ${postId}`);
|
|
}
|
|
|
|
const post = this.getPost(postId);
|
|
post.setStatus('Validated');
|
|
|
|
// Compute rewards
|
|
const rewardsAccumulator = new Map();
|
|
await this.propagateValue(rewardsAccumulator, pool, post, initialValue);
|
|
|
|
// Apply computed rewards
|
|
for (const [id, value] of rewardsAccumulator) {
|
|
bench.reputations.addTokens(id, value);
|
|
}
|
|
}
|
|
|
|
async propagateValue(rewardsAccumulator, fromActor, post, increment, depth = 0) {
|
|
if (params.referenceChainLimit >= 0 && depth > params.referenceChainLimit) {
|
|
return [];
|
|
}
|
|
|
|
this.actions.propagateValue.log(fromActor, post, `(${increment})`);
|
|
|
|
// Recursively distribute reputation to citations, according to weights
|
|
let totalOutboundAmount = 0;
|
|
let refundFromOutbound = 0;
|
|
for (const { postId: citedPostId, weight } of post.citations) {
|
|
const citedPost = this.getPost(citedPostId);
|
|
const outboundAmount = weight * increment;
|
|
totalOutboundAmount += outboundAmount;
|
|
refundFromOutbound += await this.propagateValue(rewardsAccumulator, post, citedPost, outboundAmount, depth + 1);
|
|
}
|
|
|
|
// Apply leaching value
|
|
const incrementAfterLeaching = increment - (totalOutboundAmount - refundFromOutbound) * params.leachingValue;
|
|
|
|
// Prevent value from decreasing below zero
|
|
const rawNewValue = post.value + incrementAfterLeaching;
|
|
const newValue = Math.max(0, rawNewValue);
|
|
// We "refund" the amount that could not be applied.
|
|
// Note that this will always be a negative quantity, because this situation only arises when increment is negative.
|
|
const refundToInbound = rawNewValue - newValue;
|
|
const appliedIncrement = newValue - post.value;
|
|
|
|
// Award reputation to post author
|
|
rewardsAccumulator.set(post.authorPublicKey, appliedIncrement);
|
|
|
|
// Increment the value of the post
|
|
await this.setPostValue(post, newValue);
|
|
|
|
return refundToInbound;
|
|
}
|
|
}
|