Work in progress: prototyping forum network

This commit is contained in:
Ladd Hoffman 2022-11-07 17:44:57 -06:00
parent 5036a1a8e3
commit 715943ec77
27 changed files with 2526 additions and 89 deletions

View File

@ -1,92 +1,7 @@
# Science Publishing DAO # Science Publishing DAO
## Subprojects
| Name | Description |
## Getting started | --- | --- |
| [forum-network](./forum-network) | Javascript prototyping forum architecture |
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.com/dao-governance-framework/science-publishing-dao.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](https://gitlab.com/dao-governance-framework/science-publishing-dao/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

View File

@ -0,0 +1,15 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: 'airbnb-base',
overrides: [
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
},
};

3
forum-network/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
ssl/
node_modules/
git/

1404
forum-network/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"name": "forum-network",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^8.27.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"prettier": "^2.7.1"
}
}

View File

@ -0,0 +1,53 @@
export class Action {
constructor(name, scene) {
this.name = name;
this.scene = scene;
}
log(src, dest, msg, obj, symbol = '->>') {
const logObj = false;
this.scene.log(
`${src.name} ${symbol} ${dest.name} : ${this.name} ${msg ?? ''} ${
logObj && obj ? JSON.stringify(obj) : ''
}`,
);
}
}
export class Actor {
constructor(name, scene) {
this.name = name;
this.scene = scene;
this.callbacks = new Map();
this.status = this.scene.addDisplayValue(`${this.name} status`);
this.status.set('New');
this.scene.log(`participant ${this.name}`);
}
send(dest, action, detail) {
action.log(this, dest, detail ? JSON.stringify(detail) : '');
dest.recv(this, action, detail);
return this;
}
recv(src, action, detail) {
const cb = this.callbacks.get(action.name);
if (!cb) {
throw new Error(
`[${this.scene.name} actor ${this.name} does not have a callback registered for ${action.name}`,
);
}
cb(src, detail);
return this;
}
on(action, cb) {
this.callbacks.set(action.name, cb);
return this;
}
setStatus(status) {
this.status.set(status);
return this;
}
}

View File

@ -0,0 +1,72 @@
class Blockchain {
constructor() {
this.posts = new CollectionWithReputation();
this.authors = new CollectionWithReputation();
this.nodes = new CollectionWithReputation();
}
vote(voter, batch) {
if (!this.activeVote) {
this.activeVote = new VoteInstance(batch);
this.activeVote.vote(voter, true);
return;
}
if (this.activeVote.matches(batch)) {
this.activeVote.vote(voter, true);
} else {
this.activeVote.vote(voter, false);
}
}
applyBatch(batch) {}
}
class CollectionWithReputation {
constructor() {
this.collection = new Map();
this.reputations = new Map();
}
set(id, value) {
this.collection.set(id, value);
}
get(id) {
this.collection.get(id);
}
setReputation(id, value) {
this.reputations.set(id, value);
}
get(id) {
this.reputations.get(id);
}
}
class VoteInstance {
constructor(content) {
this.content = content;
this.votes = new Map();
}
matches(content) {
return JSON.stringify(content) === JSON.stringify(this.content);
}
vote(voter, opinion) {
this.votes.set(voter.id, { voter, opinion });
}
finalize() {
const count = { for: 0, against: 0 };
for (const vote of this.votes) {
if (vote.opinion === true) {
count.for++;
} else {
count.against++;
}
}
return count.for > count.against;
}
}

View File

@ -0,0 +1,58 @@
export class CryptoUtil {
static algorithm = 'RSASSA-PKCS1-v1_5';
static hash = 'SHA-256';
static async generateSigningKey() {
return await window.crypto.subtle.generateKey(
{
name: CryptoUtil.algorithm,
hash: CryptoUtil.hash,
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
},
true,
['sign', 'verify'],
);
}
static async sign(content, privateKey) {
const encoder = new TextEncoder();
const encoded = encoder.encode(content);
const signature = await window.crypto.subtle.sign(CryptoUtil.algorithm, privateKey, encoded);
// Return base64-encoded signature
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
static async verify(content, b64publicKey, b64signature) {
// Convert base64 javascript web key to CryptoKey
const publicKey = await CryptoUtil.importKey(b64publicKey);
// Convert base64 signature to an ArrayBuffer
const signature = Uint8Array.from(atob(b64signature), (c) => c.charCodeAt(0));
// TODO: make a single TextEncoder instance and reuse it
const encoder = new TextEncoder();
const encoded = encoder.encode(content);
return await window.crypto.subtle.verify(CryptoUtil.algorithm, publicKey, signature, encoded);
}
static async exportKey(publicKey) {
// Store public key as base64 javascript web key
const jwk = await window.crypto.subtle.exportKey('jwk', publicKey);
return btoa(JSON.stringify(jwk));
}
static async importKey(b64jwk) {
// Convert base64 javascript web key to CryptoKey
const jwk = JSON.parse(atob(b64jwk));
return await window.crypto.subtle.importKey(
'jwk',
jwk,
{
name: CryptoUtil.algorithm,
hash: CryptoUtil.hash,
},
false,
['verify'],
);
}
}

View File

@ -0,0 +1,65 @@
export class DisplayValue {
constructor(name, box) {
this.value = undefined;
this.name = name;
this.box = box;
this.nameBox = this.box.addBox(`${this.name}-name`).addClass('name');
this.valueBox = this.box.addBox(`${this.name}-value`).addClass('value');
this.nameBox.setInnerHTML(this.name);
}
render() {
this.valueBox.setInnerHTML(this.value);
}
set(value) {
this.value = value;
this.render();
}
get() {
return this.value;
}
}
export class Box {
constructor(name, parentEl) {
this.el = document.createElement('div');
this.el.classList.add('box');
this.el.setAttribute('box-name', name);
if (parentEl) {
parentEl.appendChild(this.el);
}
}
flex() {
this.el.classList.add('flex');
return this;
}
monospace() {
this.el.classList.add('monospace');
return this;
}
addClass(className) {
this.el.classList.add(className);
return this;
}
addBox(name) {
const box = new Box(name);
this.el.appendChild(box.el);
return box;
}
addDisplayValue(value) {
const box = this.addBox(value.name).flex();
return new DisplayValue(value, box);
}
setInnerHTML(html) {
this.el.innerHTML = html;
return this;
}
}

View File

@ -0,0 +1,14 @@
export class ForumNetwork {
constructor() {
this.nodes = new Map();
}
addNode(node) {
this.nodes.set(node.keyPair.publicKey, node);
return this;
}
listNodes() {
return Array.from(this.nodes.values());
}
}

View File

@ -0,0 +1,106 @@
import { Actor, Action } from './actor.js';
import { Message, PostMessage, PeerMessage } from './message.js';
import { CryptoUtil } from './crypto.js';
import { ForumView } from './forum-view.js';
import { PrioritizedQueue } from './prioritized-queue.js';
export class ForumNode extends Actor {
constructor(name, scene) {
super(name, scene);
this.forumView = new ForumView();
this.queue = new PrioritizedQueue();
this.actions = {
storePost: new Action('store post', scene),
peerMessage: new Action('peer message', scene),
};
}
// Generate a signing key pair and connect to the network
async initialize(forumNetwork) {
this.keyPair = await CryptoUtil.generateSigningKey();
this.forumNetwork = forumNetwork.addNode(this);
this.status.set('Initialized');
return this;
}
// Send a message to all other nodes in the network
async broadcast(message) {
await message.sign(this.keyPair);
const otherForumNodes = this.forumNetwork
.listNodes()
.filter((forumNode) => forumNode.keyPair.publicKey !== this.keyPair.publicKey);
for (const forumNode of otherForumNodes) {
// For now just call receiveMessage on the target node
this.actions.peerMessage.log(this, forumNode, null, message.content);
await forumNode.receiveMessage(JSON.stringify(message.toJSON()));
}
}
// Perform minimal processing to ingest a message.
// Enqueue it for further processing.
async receiveMessage(messageStr) {
const messageJson = JSON.parse(messageStr);
const senderReputation = this.forumView.getReputation(messageJson.publicKey) || 0;
this.queue.add(messageJson, senderReputation);
}
// Process next highest priority message in the queue
async processNextMessage() {
const messageJson = this.queue.pop();
if (!messageJson) {
return null;
}
await this.processMessage(messageJson);
}
// Process a message from the queue
async processMessage(messageJson) {
try {
await Message.verify(messageJson);
} catch (e) {
this.actions.processMessage.log(this, this, 'invalid signature', messageJson, '-x');
console.log(`${this.name}: received message with invalid signature`);
return;
}
const { publicKey } = messageJson;
const message = Message.fromJSON(messageJson);
console.log(`${this.name}: processMessage`, message);
if (message instanceof PostMessage) {
await this.processPostMessage(publicKey, message.content);
} else if (message instanceof PeerMessage) {
await this.processPeerMessage(publicKey, message.content);
} else {
// Unknown message type
// Penalize sender for wasting our time
console.log(`${this.name}: penalizing sender for unknown message type ${message.type}`);
this.forumView.incrementReputation(message.publicKey, -1);
}
}
// Process an incoming post, received by whatever means
processPost(authorId, post, stake) {
this.actions.storePost.log(this, this, null, { authorId, post, stake });
this.forumView.addPost(authorId, post.id, post, stake);
}
// Process a post we received in a message
async processPostMessage(authorId, { post, stake }) {
this.processPost(authorId, post, stake);
await this.broadcast(
new PeerMessage({
posts: [{ authorId, post, stake }],
}),
);
}
// Process a message we receive from a peer
async processPeerMessage(peerId, { posts }) {
// We are trusting that the peer verified the signatures of the posts they're forwarding.
// We could instead have the peer forward the signed messages and re-verify them.
for (const { authorId, post, stake } of posts) {
this.processPost(authorId, post, stake);
}
}
}

View File

@ -0,0 +1,122 @@
import { Graph } from './graph.js';
class Author {
constructor() {
this.posts = new Map();
this.reputation = 0;
}
}
// TODO: Consider merging with "client-side" Post class in `./post.js`
class Post {
constructor(id, author, stake, content, citations) {
this.id = id;
this.author = author;
this.content = content;
this.stake = stake;
this.citations = citations;
this.reputation = 0;
}
}
export class ForumView {
constructor() {
this.reputations = new Map();
this.posts = new Graph();
this.authors = new Map();
}
getReputation(publicKey) {
return this.reputations.get(publicKey);
}
incrementReputation(publicKey, increment, reason) {
const reputation = this.getReputation(publicKey) || 0;
return this.reputations.set(publicKey, reputation + increment);
}
getOrInitializeAuthor(authorId) {
let author = this.authors.get(authorId);
if (!author) {
author = new Author(authorId);
this.authors.set(authorId, author);
}
return author;
}
addPost(authorId, postId, { citations = [], content }, stake) {
const author = this.getOrInitializeAuthor(authorId);
const post = new Post(postId, author, stake, content, citations);
this.posts.addVertex(postId, post);
for (const citation of citations) {
this.posts.addEdge('citation', postId, citation.postId, citation);
}
this.applyNonbindingReputationEffects(post);
}
getPost(postId) {
return this.posts.getVertexData(postId);
}
getPosts() {
return this.posts.getVertices();
}
// We'll start with naieve implementations of the computations we need.
// We want to derive a value -- maybe call it a reputation score -- for each post.
// This value is a recursive sum of contributions from citations.
// There should be a diminishment of effect upon each recursion,
// perhaps following a geometric progression.
// Each post gets some initial score due to the reputation that the author stakes.
// Citations are weighted, and can be positive or negative.
// So each post has a reputation score. Each author also has a reputation score.
// The value of the author's reputation score could be a factor in the magnitude of the effects of their citations.
// Post_rep = (Author_rep * stake);
//
// Options:
// - update a state model incrementally with each action in the history (/unfolding present) of the forum,
// in order to arrive at the current view.
// When an author stakes reputation on a post, if it's a non-binding stake, then it merely expresses opinion.
// If it's a binding stake, then they may lose the staked reputation as a result of other posts staking reputation against theirs.
citationFraction = 0.3;
applyNonbindingReputationEffects(newPost) {
this.distributeNonbindingReputation(newPost, newPost, newPost.stake);
}
distributeNonbindingReputation(newPost, post, amount, depth = 0) {
console.log('distributeNonbindingReputation', { post, amount, depth });
// Some of the incoming reputation goes to this post
post.reputation += amount * (1 - this.citationFraction);
// Some of the incoming reputation gets distributed among cited posts
const distributeAmongCitations = amount * this.citationFraction;
// citation weights can be interpreted as a ratio, or we can somehow constrain the input to add up to some specified total.
// It's easy enough to let them be on any arbitrary scale and just compute the ratios here.
const totalWeight = post.citations
?.map(({ weight }) => weight)
.reduce((acc, cur) => (acc += cur), 0);
post.citations?.forEach((citation) => {
const citedPost = this.getPost(citation.postId);
if (!citedPost) {
// TODO: Here is where we may want to engage our peer protocol to query for possible missing records
throw new Error(`Post ${post.postId} cites unknown post ${citation.postId}`);
}
this.distributeNonbindingReputation(
newPost,
citedPost,
(citation.weight / totalWeight) * distributeAmongCitations,
depth + 1,
);
});
}
}

View File

@ -0,0 +1,85 @@
export class Vertex {
constructor(data) {
this.data = data;
this.edges = {
from: [],
to: [],
};
}
}
export class Edge {
constructor(label, from, to, data) {
this.from = from;
this.to = to;
this.label = label;
this.data = data;
}
}
export class Graph {
constructor() {
this.vertices = new Map();
this.edgeLabels = new Map();
this.nextVertexId = 0;
}
addVertex(id, data) {
// Support simple case of auto-incremented numeric ids
if (typeof id === 'object') {
data = id;
id = this.nextVertexId++;
}
const vertex = new Vertex(data);
this.vertices.set(id, vertex);
return this;
}
getVertex(id) {
return this.vertices.get(id);
}
getVertexData(id) {
return this.getVertex(id)?.data;
}
getVertices() {
return Array.from(this.vertices.values()).map(({ data }) => data);
}
getEdge(label, from, to) {
const edges = this.edgeLabels.get(label);
return edges?.get(JSON.stringify({ from, to }));
}
setEdge(label, from, to, edge) {
let edges = this.edgeLabels.get(label);
if (!edges) {
edges = new Map();
this.edgeLabels.set(label, edges);
}
edges.set(JSON.stringify({ from, to }), edge);
}
addEdge(label, from, to, data) {
if (this.getEdge(label, from, to)) {
throw new Error(`Edge ${label} from ${from} to ${to} already exists`);
}
const edge = new Edge(label, from, to, data);
this.setEdge(label, from, to, edge);
this.getVertex(from).edges.from.push(edge);
this.getVertex(to).edges.to.push(edge);
return this;
}
getEdges(label, from, to) {
const edgeLabels = label ? [label] : Array.from(this.edgeLabels.keys());
return edgeLabels.flatMap((edgeLabel) => {
const edges = this.edgeLabels.get(edgeLabel);
return Array.from(edges?.values() || []).filter((edge) => {
const matchFrom = from === null || from === undefined || from === edge.from;
const matchTo = to === null || to === undefined || to === edge.to;
return matchFrom && matchTo;
});
});
}
}

View File

@ -0,0 +1,26 @@
import { Actor, Action } from './actor.js';
import { PostMessage } from './message.js';
import { CryptoUtil } from './crypto.js';
export class Member extends Actor {
constructor(name, scene) {
super(name, scene);
this.actions = {
submitPost: new Action('submit post', scene),
};
}
async initialize() {
this.keyPair = await CryptoUtil.generateSigningKey();
this.status.set('Initialized');
return this;
}
async submitPost(forumNode, post, stake) {
const postMessage = new PostMessage({ post, stake });
await postMessage.sign(this.keyPair);
this.actions.submitPost.log(this, forumNode, null, { id: post.id });
// For now, directly call forumNode.receiveMessage();
await forumNode.receiveMessage(JSON.stringify(postMessage.toJSON()));
}
}

View File

@ -0,0 +1,69 @@
import { CryptoUtil } from './crypto.js';
import { Post } from './post.js';
export class Message {
constructor(content) {
this.content = content;
}
async sign({ publicKey, privateKey }) {
this.publicKey = await CryptoUtil.exportKey(publicKey);
// Call toJSON before signing, to match what we'll later send
this.signature = await CryptoUtil.sign(this.contentToJSON(this.content), privateKey);
return this;
}
static async verify({ content, publicKey, signature }) {
return await CryptoUtil.verify(content, publicKey, signature);
}
static contentFromJSON(data) {
return data;
}
contentToJSON(content) {
return content;
}
static fromJSON({ type, content }) {
const messageType = messageTypes.get(type) || Message;
const messageContent = messageType.contentFromJSON(content);
return new messageType(messageContent);
}
toJSON() {
return {
type: this.type,
content: this.contentToJSON(this.content),
publicKey: this.publicKey,
signature: this.signature,
};
}
}
export class PostMessage extends Message {
type = 'post';
static contentFromJSON({ post, stake }) {
return {
post: Post.fromJSON(post),
stake,
};
}
contentToJSON({ post, stake }) {
return {
post: post.toJSON(),
stake,
};
}
}
export class PeerMessage extends Message {
type = 'peer';
}
const messageTypes = new Map([
['post', PostMessage],
['peer', PeerMessage],
]);

View File

@ -0,0 +1,46 @@
export class Post {
constructor(content) {
this.id = crypto.randomUUID();
this.content = content;
this.citations = [];
}
addCitation(postId, weight) {
const citation = new Citation(postId, weight);
this.citations.push(citation);
return this;
}
toJSON() {
return {
id: this.id,
content: this.content,
citations: this.citations.map((citation) => citation.toJSON()),
};
}
static fromJSON({ id, content, citations }) {
const post = new Post(content);
post.id = id;
post.citations = citations.map((citation) => Citation.fromJSON(citation));
return post;
}
}
export class Citation {
constructor(postId, weight) {
this.postId = postId;
this.weight = weight;
}
toJSON() {
return {
postId: this.postId,
weight: this.weight,
};
}
static fromJSON({ postId, weight }) {
return new Citation(postId, weight);
}
}

View File

@ -0,0 +1,24 @@
export class PrioritizedQueue {
constructor() {
this.buffer = [];
}
// Add an item to the buffer, ahead of the next lowest priority item
add(message, priority) {
const idx = this.buffer.findIndex((item) => item.priority < priority);
if (idx < 0) {
this.buffer.push({ message, priority });
} else {
this.buffer.splice(idx, 0, { message, priority });
}
}
// Return the highest priority item in the buffer
pop() {
if (!this.buffer.length) {
return null;
}
const item = this.buffer.shift();
return item.message;
}
}

View File

@ -0,0 +1,3 @@
class Reputation {
constructor() {}
}

View File

@ -0,0 +1,33 @@
import { Actor, Action } from './actor.js';
export class Scene {
constructor(name, rootBox) {
this.name = name;
this.box = rootBox.addBox(name);
this.titleBox = this.box.addBox().setInnerHTML(name);
this.box.addBox('Spacer').setInnerHTML('&nbsp;');
this.displayValuesBox = this.box.addBox(`${this.name}-values`);
this.box.addBox('Spacer').setInnerHTML('&nbsp;');
this.logBox = this.box.addBox(`${this.name}-log`);
}
addActor(name) {
const actor = new Actor(name, this);
return actor;
}
addAction(name) {
const action = new Action(name, this);
return action;
}
addDisplayValue(name) {
const dv = this.displayValuesBox.addDisplayValue(name);
return dv;
}
log(msg) {
this.logBox.addBox().setInnerHTML(msg).monospace();
return this;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<head>
<title>Forum</title>
<script type="module" src="./forum-test.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="forum-network"></div>
</body>

View File

@ -0,0 +1,40 @@
import { Box } from './classes/display-value.js';
import { Scene } from './classes/scene.js';
import { Post } from './classes/post.js';
import { Member } from './classes/member.js';
import { ForumNode } from './classes/forum-node.js';
import { ForumNetwork } from './classes/forum-network.js';
const delay = async (ms) => {
await new Promise((resolve) => setTimeout(resolve, ms));
};
const rootElement = document.getElementById('forum-network');
const rootBox = new Box('rootBox', rootElement).flex();
window.scene = new Scene('Forum test', rootBox).log('sequenceDiagram');
window.author1 = await new Member('author1', window.scene).initialize();
window.author2 = await new Member('author2', window.scene).initialize();
window.forumNetwork = new ForumNetwork();
window.forumNode1 = await new ForumNode('node1', window.scene).initialize(window.forumNetwork);
window.forumNode2 = await new ForumNode('node2', window.scene).initialize(window.forumNetwork);
window.forumNode3 = await new ForumNode('node3', window.scene).initialize(window.forumNetwork);
setInterval(async () => {
await window.forumNode1.processNextMessage();
await window.forumNode2.processNextMessage();
await window.forumNode3.processNextMessage();
}, 100);
// const blockchain = new Blockchain();
window.post1 = new Post({ message: 'hi' });
window.post2 = new Post({ message: 'hello' }).addCitation(window.post1.id, 1.0);
await delay(1000);
await window.author1.submitPost(window.forumNode1, window.post1, 50);
await delay(1000);
await window.author2.submitPost(window.forumNode2, window.post2, 100);

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<head>
<title>Forum Graph</title>
<script type="module" src="./graph-test.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="graph-test"></div>
</body>

View File

@ -0,0 +1,23 @@
import { Box } from './classes/display-value.js';
import { Scene } from './classes/scene.js';
import { Graph } from './classes/graph.js';
const rootElement = document.getElementById('graph-test');
const rootBox = new Box('rootBox', rootElement).flex();
window.scene = new Scene('Graph test', rootBox);
window.graph = new Graph();
window.v = [];
function addVertex() {
const vertex = window.graph.addVertex({ seq: window.v.length });
window.v.push(vertex);
}
addVertex();
addVertex();
addVertex();
addVertex();
addVertex();
window.graph.addEdge('e1', 0, 1);

View File

@ -0,0 +1,24 @@
.box {
// border: 1px #eee solid;
width: fit-content;
font-family: sans-serif;
font-size: 12pt;
}
.box .name {
width: 10em;
font-weight: bold;
text-align: right;
margin-right: 6pt;
}
.box .value {
width: fit-content;
// border: 0px;
}
.flex {
display: flex;
}
.monospace {
// border: 0px;
font-family: monospace;
font-size: 11pt;
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<head>
<title>Forum Network</title>
<script src="./classes/display-value.js"></script>
<script src="./classes/actor.js"></script>
<script src="./classes/scene.js"></script>
<script src="./classes/reputation.js"></script>
<script src="./classes/forum.js"></script>
<script src="./index.js" defer></script>
<link type="text/css" rel="stylesheet" href="./index.css" />
</head>
<body>
<div id="forum-network"></div>
</body>

View File

@ -0,0 +1,188 @@
const rootElement = document.getElementById('forum-network');
const rootBox = new Box('rootBox', rootElement).flex();
function randomDelay(min, max) {
const delayMs = min + Math.random() * max;
return delayMs;
}
function delay(min, max = min) {
const delayMs = min + Math.random() * (max - min);
return new Promise((resolve) => setTimeout(resolve, delayMs));
}
if (true) {
const scene = new Scene('Scene 1', rootBox);
const webClientStatus = scene.addDisplayValue('WebClient Status');
const node1Status = scene.addDisplayValue('Node 1 Status');
const blockchainStatus = scene.addDisplayValue('Blockchain Status');
const webClient = scene.addActor('web client');
const node1 = scene.addActor('node 1');
const blockchain = scene.addActor('blockchain');
const requestForumPage = scene.addAction('requestForumPage');
const readBlockchainData = scene.addAction('readBlockchainData');
const blockchainData = scene.addAction('blockchainData');
const forumPage = scene.addAction('forumPage');
webClientStatus.set('Initialized');
node1Status.set('Idle');
blockchainStatus.set('Idle');
node1.on(requestForumPage, (src, detail) => {
node1Status.set('Processing request');
node1.on(blockchainData, (_src, data) => {
node1Status.set('Processing response');
setTimeout(() => {
node1.send(src, forumPage, data);
node1Status.set('Idle');
}, randomDelay(500, 1000));
});
setTimeout(() => {
node1.send(blockchain, readBlockchainData, detail);
}, randomDelay(500, 1500));
});
blockchain.on(readBlockchainData, (src, detail) => {
blockchainStatus.set('Processing request');
setTimeout(() => {
blockchain.send(src, blockchainData, {});
blockchainStatus.set('Idle');
}, randomDelay(500, 1500));
});
webClient.on(forumPage, (_src, _detail) => {
webClientStatus.set('Received forum page');
});
setInterval(() => {
webClient.send(node1, requestForumPage);
webClientStatus.set('Requested forum page');
}, randomDelay(6000, 12000));
}
(async function () {
const scene = new Scene('Scene 2', rootBox);
const webClient = scene.addActor('webClient');
const nodes = [];
const memories = [];
const storages = [];
function addNode() {
const idx = nodes.length;
const node = scene.addActor(`node${idx}`);
const memory = scene.addActor(`memory${idx}`);
const storage = scene.addActor(`storage${idx}`);
node.memory = memory;
node.storage = storage;
nodes.push(node);
memories.push(memory);
storages.push(storage);
return node;
}
function getPeer(node) {
const peers = nodes.filter((peer) => peer !== node);
const idx = Math.floor(Math.random() * peers.length);
return peers[idx];
}
addNode();
addNode();
const [
seekTruth,
considerInfo,
evaluateConfidence,
chooseResponse,
qualifiedOpinions,
requestMemoryData,
memoryData,
requestStorageData,
storageData,
] = [
'seek truth',
'consider available information',
'evaluate confidence',
'choose response',
'qualified opinions',
'request in-memory data',
'in-memory data',
'request storage data',
'storage data',
].map((name) => scene.addAction(name));
memories.forEach((memory) => {
memory.setStatus('Idle');
memory.on(requestMemoryData, async (src, _detail) => {
memory.setStatus('Retrieving data');
await delay(1000);
memory.send(src, memoryData, {});
memory.setStatus('Idle');
});
});
storages.forEach((storage) => {
storage.setStatus('Idle');
storage.on(requestStorageData, async (src, _detail) => {
storage.setStatus('Retrieving data');
await delay(1000);
storage.send(src, storageData, {});
storage.setStatus('Idle');
});
});
nodes.forEach((node) => {
node.setStatus('Idle');
node.on(seekTruth, async (seeker, detail) => {
node.setStatus('Processing request');
node.on(chooseResponse, async (_src, _info) => {
node.setStatus('Choosing response');
await delay(1000);
node.send(seeker, qualifiedOpinions, {});
node.setStatus('Idle');
});
node.on(evaluateConfidence, async (_src, _info) => {
node.setStatus('Evaluating confidence');
await delay(1000);
node.send(node, chooseResponse);
});
node.on(considerInfo, async (_src, _info) => {
node.setStatus('Considering info');
await delay(1000);
node.send(node, evaluateConfidence);
});
node.on(memoryData, (_src, _data) => {
node.on(storageData, (_src, _data) => {
if (detail?.readConcern === 'single') {
node.send(node, considerInfo, {});
} else {
const peer = getPeer(node);
node.on(qualifiedOpinions, (_src, info) => {
node.send(node, considerInfo, info);
});
node.send(peer, seekTruth, { readConcern: 'single' });
}
});
node.send(node.storage, requestStorageData);
});
await delay(1000);
node.send(node.memory, requestMemoryData);
});
});
webClient.on(qualifiedOpinions, (_src, _detail) => {
webClient.setStatus('Received opinions and qualifications');
});
await delay(1000);
webClient.setStatus('Seek truth');
webClient.send(nodes[0], seekTruth);
}());