graph-js: useBigIntOrderedMap

This commit is contained in:
Liam Fitzgerald 2020-10-29 12:52:13 +10:00
parent 225718b8cf
commit c6dc485d24
12 changed files with 324 additions and 107 deletions

View File

@ -2666,6 +2666,11 @@
"integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
"dev": true
},
"big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
},
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@ -10112,7 +10117,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -10133,12 +10139,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -10153,17 +10161,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -10280,7 +10291,8 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -10292,6 +10304,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -10306,6 +10319,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -10313,12 +10327,14 @@
"minimist": {
"version": "1.2.5",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -10337,6 +10353,7 @@
"version": "0.5.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
@ -10398,7 +10415,8 @@
"npm-normalize-package-bin": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.4.8",
@ -10426,7 +10444,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -10438,6 +10457,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -10515,7 +10535,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -10551,6 +10572,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -10570,6 +10592,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -10613,12 +10636,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@ -11099,7 +11124,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -11120,12 +11146,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -11140,17 +11168,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -11267,7 +11298,8 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -11279,6 +11311,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -11293,6 +11326,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -11300,12 +11334,14 @@
"minimist": {
"version": "1.2.5",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -11324,6 +11360,7 @@
"version": "0.5.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
@ -11385,7 +11422,8 @@
"npm-normalize-package-bin": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.4.8",
@ -11413,7 +11451,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -11425,6 +11464,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -11502,7 +11542,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -11538,6 +11579,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -11557,6 +11599,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -11600,12 +11643,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},

View File

@ -12,6 +12,7 @@
"@tlon/indigo-react": "urbit/indigo-react#lf/1.2.9",
"@tlon/sigil-js": "^1.4.2",
"aws-sdk": "^2.726.0",
"big-integer": "^1.6.48",
"classnames": "^2.2.6",
"codemirror": "^5.55.0",
"css-loader": "^3.5.3",

View File

@ -0,0 +1,193 @@
import bigInt, { BigInteger } from "big-integer";
interface NonemptyNode<V> {
n: [BigInteger, V];
l: MapNode<V>;
r: MapNode<V>;
}
type MapNode<V> = NonemptyNode<V> | null;
/**
* An implementation of ordered maps for JS
* Plagiarised wholesale from sys/zuse
*/
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
private root: MapNode<V> = null;
size: number = 0;
constructor() {}
/**
* Retrieve an value for a key
*/
get(key: BigInteger): V | null {
const inner = (node: MapNode<V>) => {
if (!node) {
return node;
}
const [k, v] = node.n;
if (key.eq(k)) {
return v;
}
if (key.gt(k)) {
return inner(node.l);
} else {
return inner(node.r);
}
};
return inner(this.root);
}
/**
* Put an item by a key
*/
set(key: BigInteger, value: V): void {
const inner = (node: MapNode<V>) => {
if (!node) {
return {
n: [key, value],
l: null,
r: null,
};
}
const [k] = node.n;
if (key.eq(k)) {
this.size--;
return {
...node,
n: [k, value],
};
}
if (key.gt(k)) {
const l = inner(node.l);
if (!l) {
throw new Error("invariant violation");
}
return {
...node,
l,
};
}
const r = inner(node.r);
if (!r) {
throw new Error("invariant violation");
}
return { ...node, r };
};
this.size++;
this.root = inner(this.root);
}
/**
* Remove all entries
*/
clear() {
this.root = null;
}
/**
* Predicate testing if map contains key
*/
has(key: BigInteger): boolean {
const inner = (node: MapNode<V>) => {
if (!node) {
return false;
}
const [k] = node.n;
if (k.eq(key)) {
return true;
}
if (key.gt(k)) {
return inner(node.l);
}
return inner(node.r);
};
return inner(this.root);
}
/**
* Remove value associated with key, returning whether that key
* existed in the first place
*/
delete(key: BigInteger) {
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
if (!node) {
return [false, null];
}
const [k] = node.n;
if (k.eq(key)) {
return [true, this.nip(node)];
}
if (key.gt(k)) {
const [bool, l] = inner(node.l);
return [
bool,
{
...node,
l,
},
];
}
const [bool, r] = inner(node.r);
return [
bool,
{
...node,
r,
},
];
};
const [ret, newRoot] = inner(this.root);
if(ret) {
this.size--;
}
this.root = newRoot;
return ret;
}
private nip(nod: NonemptyNode<V>): MapNode<V> {
const inner = (node: NonemptyNode<V>) => {
if (!node.l) {
return node.r;
}
if (!node.r) {
return node.l;
}
return {
...node.l,
r: inner(node.r),
};
};
return inner(nod);
}
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
let result: [BigInteger, V][] = [];
const inner = (node: MapNode<V>) => {
if (!node) {
return;
}
inner(node.l);
result.push(node.n);
inner(node.r);
};
inner(this.root);
let idx = 0;
return {
[Symbol.iterator]: this[Symbol.iterator],
next: (): IteratorResult<[BigInteger, V]> => {
if (idx < result.length) {
return { value: result[idx++], done: false };
}
return { done: true, value: null };
},
};
}
}

View File

@ -1,5 +1,7 @@
import { Post, GraphNode, TextContent, Graph, NodeMap } from "~/types";
import { buntPost } from '~/logic/lib/post';
import {BigIntOrderedMap} from "./BigIntOrderedMap";
import bigInt from 'big-integer';
export function newPost(
title: string,
@ -73,7 +75,7 @@ export function editPost(rev: number, noteId: number, title: string, body: strin
}
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
const revs = node.children.get(1);
const revs = node.children.get(bigInt(1));
const empty = [1, "", "", buntPost()] as [number, string, string, Post];
if(!revs) {
return empty;
@ -87,9 +89,9 @@ export function getLatestRevision(node: GraphNode): [number, string, string, Pos
}
export function getComments(node: GraphNode): GraphNode {
const comments = node.children.get(2);
const comments = node.children.get(bigInt(2));
if(!comments) {
return { post: buntPost(), children: new Map() }
return { post: buntPost(), children: new BigIntOrderedMap() }
}
return comments;
}

View File

@ -1,16 +1,6 @@
import _ from 'lodash';
import { OrderedMap } from "~/logic/lib/OrderedMap";
const DA_UNIX_EPOCH = 170141184475152167957503069145530368000;
const normalizeKey = (key) => {
if(key > DA_UNIX_EPOCH) {
// new links uses milliseconds since unix epoch
// old (pre-graph-store) use @da
// ported from +time:enjs:format in hoon.hoon
return Math.round((1000 * (9223372036854775 + (key - DA_UNIX_EPOCH))) / 18446744073709551616);
}
return key;
}
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
import bigInt, { BigInteger } from "big-integer";
export const GraphReducer = (json, state) => {
const data = _.get(json, 'graph-update', false);
@ -38,33 +28,26 @@ const addGraph = (json, state) => {
const _processNode = (node) => {
// is empty
if (!node.children) {
node.children = new OrderedMap();
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
node.children = new BigIntOrderedMap();
return node;
}
// is graph
let converted = new OrderedMap();
let converted = new BigIntOrderedMap();
for (let i in node.children) {
let item = node.children[i];
let index = item[0].split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
if (index.length === 0) { break; }
const normalKey = normalizeKey(index[index.length - 1]);
item[1].post.originalKey = index[index.length - 1];
converted.set(
normalKey,
index[index.length - 1],
_processNode(item[1])
);
}
node.children = converted;
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
return node;
};
@ -75,21 +58,22 @@ const addGraph = (json, state) => {
}
let resource = data.resource.ship + '/' + data.resource.name;
state.graphs[resource] = new OrderedMap();
state.graphs[resource] = new BigIntOrderedMap();
for (let i in data.graph) {
let item = data.graph[i];
let index = item[0].split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
if (index.length === 0) { break; }
let node = _processNode(item[1]);
const normalKey = normalizeKey(index[index.length - 1])
node.post.originalKey = index[index.length - 1];
state.graphs[resource].set(normalKey, node);
state.graphs[resource].set(
index[index.length - 1],
node
);
}
state.graphKeys.add(resource);
}
@ -108,7 +92,7 @@ const removeGraph = (json, state) => {
};
const mapifyChildren = (children) => {
return new OrderedMap(
return new BigIntOrderedMap(
children.map(([idx, node]) => {
const nd = {...node, children: mapifyChildren(node.children || []) };
return [normalizeKey(parseInt(idx.slice(1), 10)), nd];
@ -119,23 +103,18 @@ const addNodes = (json, state) => {
const _addNode = (graph, index, node) => {
// set child of graph
if (index.length === 1) {
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
const normalKey = normalizeKey(index[0])
node.post.originalKey = index[0];
graph.set(normalKey, node);
graph.set(index[0], node);
return graph;
}
// set parent of graph
let parNode = graph.get(normalizeKey(index[0]));
let parNode = graph.get(index[0]);
if (!parNode) {
console.error('parent node does not exist, cannot add child');
return;
}
parNode.children = _addNode(parNode.children, index.slice(1), node);
graph.set(normalizeKey(index[0]), parNode);
graph.set(index[0], parNode);
return graph;
};
@ -151,7 +130,7 @@ const addNodes = (json, state) => {
if (item[0].split('/').length === 0) { return; }
let index = item[0].split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
if (index.length === 0) { return; }
@ -174,9 +153,9 @@ const removeNodes = (json, state) => {
if (index.length === 1) {
graph.delete(index[0]);
} else {
const child = graph.get(normalizeKey(index[0]));
const child = graph.get(index[0]);
_remove(child.children, index.slice(1));
graph.set(normalizeKey(index[0]), child);
graph.set(index[0], child);
}
};
const data = _.get(json, 'remove-nodes', false);
@ -188,7 +167,7 @@ const removeNodes = (json, state) => {
data.indices.forEach((index) => {
if (index.split('/').length === 0) { return; }
let indexArr = index.split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
_remove(state.graphs[res], indexArr);
});

View File

@ -1,4 +1,5 @@
import {Patp} from "./noun";
import { Patp } from "./noun";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
export interface TextContent { text: string; };
@ -23,7 +24,7 @@ export interface GraphNode {
post: Post;
}
export type Graph = Map<number, GraphNode>;
export type Graph = BigIntOrderedMap<GraphNode>;
export type Graphs = { [rid: string]: Graph };

View File

@ -1,6 +1,7 @@
import React, { useEffect } from "react";
import { Box, Row, Col, Center, LoadingSpinner } from "@tlon/indigo-react";
import { Switch, Route, Link } from "react-router-dom";
import bigInt from 'big-integer';
import GlobalApi from "~/logic/api/global";
import { StoreState } from "~/logic/store/type";
@ -99,7 +100,7 @@ export function LinkResource(props: LinkResourceProps) {
return <div>Malformed URL</div>;
}
const index = parseInt(indexArr[1], 10);
const index = bigInt(indexArr[1]);
const node = !!graph ? graph.get(index) : null;
if (!node) {
@ -124,7 +125,7 @@ export function LinkResource(props: LinkResourceProps) {
name={name}
ship={ship}
api={api}
parentIndex={node.post.originalIndex}
parentIndex={node.post.index}
/>
</Row>
<Comments

View File

@ -1,6 +1,8 @@
import React, { useState, useEffect } from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import ReactMarkdown from "react-markdown";
import bigInt from 'big-integer';
import { Link, RouteComponentProps } from "react-router-dom";
import { Spinner } from "~/views/components/Spinner";
import { Comments } from "./Comments";
@ -19,7 +21,6 @@ interface NoteProps {
api: GlobalApi;
hideAvatars: boolean;
hideNicknames: boolean;
baseUrl?: string;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
rootUrl: string;
baseUrl: string;
@ -40,7 +41,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
const comments = getComments(note);
const [revNum, title, body, post] = getLatestRevision(note);
const noteId = parseInt(note.post.index.split('/')[1], 10);
const noteId = bigInt(note.post.index.split('/')[1]);
let adminLinks: JSX.Element | null = null;
if (window.ship === note?.post?.author) {

View File

@ -2,8 +2,9 @@ import React, { Component } from "react";
import moment from "moment";
import { Box } from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import {Graph, GraphNode} from "~/types";
import {getLatestRevision} from "~/logic/lib/publish";
import { Graph, GraphNode } from "~/types";
import { getLatestRevision } from "~/logic/lib/publish";
import { BigInteger } from "big-integer";
function NavigationItem(props: {
url: string;
@ -31,17 +32,20 @@ function NavigationItem(props: {
);
}
function getAdjacentId(graph: Graph, child: number, backwards = false): number | null {
function getAdjacentId(
graph: Graph,
child: BigInteger,
backwards = false
): BigInteger | null {
const children = Array.from(graph);
const i = children.findIndex(([index]) => index === child);
const target = children[backwards ? i+1 : i-1];
const i = children.findIndex(([index]) => index.eq(child));
const target = children[backwards ? i + 1 : i - 1];
return target?.[0] || null;
}
function makeNoteUrl(noteId: number) {
return noteId.toString();
}
interface NoteNavigationProps {
noteId: number;
@ -53,7 +57,7 @@ export function NoteNavigation(props: NoteNavigationProps) {
let nextComponent = <Box />;
let prevComponent = <Box />;
const { noteId, notebook } = props;
if(!notebook) {
if (!notebook) {
return null;
}
const nextId = getAdjacentId(notebook, noteId);
@ -67,27 +71,16 @@ export function NoteNavigation(props: NoteNavigationProps) {
if (next && nextId) {
const nextUrl = makeNoteUrl(nextId);
const [,title,, post] = getLatestRevision(next);
const date = post['time-sent'];
nextComponent = (
<NavigationItem
title={title}
date={date}
url={nextUrl}
/>
);
const [, title, , post] = getLatestRevision(next);
const date = post["time-sent"];
nextComponent = <NavigationItem title={title} date={date} url={nextUrl} />;
}
if (prev && prevId) {
const prevUrl = makeNoteUrl(prevId);
const [,title,, post] = getLatestRevision(prev);
const date = post['time-sent'];
const [, title, , post] = getLatestRevision(prev);
const date = post["time-sent"];
prevComponent = (
<NavigationItem
title={title}
date={date}
url={prevUrl}
prev
/>
<NavigationItem title={title} date={date} url={prevUrl} prev />
);
}

View File

@ -19,7 +19,7 @@ export function NotebookPosts(props: NotebookPostsProps) {
([date, node]) =>
node && (
<NotePreview
key={date}
key={date.toString()}
host={props.host}
book={props.book}
contact={props.contacts[node.post.author]}

View File

@ -12,6 +12,7 @@ import {
} from "~/types";
import { Center, LoadingSpinner } from "@tlon/indigo-react";
import { Notebook as INotebook } from "~/types/publish-update";
import bigInt, { BigInteger } from 'big-integer';
import Notebook from "./Notebook";
import NewPost from "./new-post";
@ -82,7 +83,7 @@ export function NotebookRoutes(
path={relativePath("/note/:noteId")}
render={(routeProps) => {
const { noteId } = routeProps.match.params;
const noteIdNum = parseInt(noteId, 10);
const noteIdNum = bigInt(noteId)
if(!graph) {
return <Center height="100%"><LoadingSpinner /></Center>;

View File

@ -24,12 +24,12 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
}
}
const getAppIcon = (app: string, module: string) => {
const getAppIcon = (app: string, mod: string) => {
if (app === "graph") {
if (module === "link") {
if (mod === "link") {
return "Links";
}
return _.capitalize(module);
return _.capitalize(mod);
}
return _.capitalize(app);
};
@ -58,7 +58,7 @@ export function SidebarItem(props: {
const { association, path, selected, apps, groups } = props;
const title = getItemTitle(association);
const appName = association?.["app-name"];
const module = association?.metadata?.module || appName;
const mod = association?.metadata?.module || appName;
const appPath = association?.["app-path"];
const groupPath = association?.["group-path"];
const app = apps[appName];
@ -74,8 +74,8 @@ export function SidebarItem(props: {
const baseUrl = isUnmanaged ? `/~landscape/home` : `/~landscape${groupPath}`;
const to = isSynced
? `${baseUrl}/resource/${module}${appPath}`
: `${baseUrl}/join/${module}${appPath}`;
? `${baseUrl}/resource/${mod}${appPath}`
: `${baseUrl}/join/${mod}${appPath}`;
const color = selected ? "black" : isSynced ? "gray" : "lightGray";
@ -101,7 +101,7 @@ export function SidebarItem(props: {
<Icon
display="block"
color={color}
icon={getAppIcon(appName, module) as any}
icon={getAppIcon(appName, mod) as any}
/>
<Box width='100%' flexShrink={2} ml={2} display='flex' overflow='hidden'>
<Text