Selaa lähdekoodia

List editor prototype

Joeri Exelmans 2 vuotta sitten
vanhempi
commit
c6afa91260

+ 2 - 2
package.json

@@ -12,14 +12,14 @@
 		"@types/node": "^18.14.6",
 		"@types/react": "^18.0.28",
 		"@types/react-dom": "^18.0.11",
+		"fork-ts-checker-webpack-plugin": "^7.3.0",
 		"mocha": "^10.2.0",
 		"nyc": "^15.1.0",
 		"ts-loader": "^9.4.2",
 		"url-loader": "^4.1.1",
 		"webpack": "^5.76.0",
 		"webpack-cli": "^5.0.1",
-		"webpack-dev-server": "^4.11.1",
-		"fork-ts-checker-webpack-plugin": "^7.3.0"
+		"webpack-dev-server": "^4.11.1"
 	},
 	"dependencies": {
 		"allotment": "^1.18.1",

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 337 - 312
pnpm-lock.yaml


+ 7 - 2
src/frontend/app.tsx

@@ -5,20 +5,21 @@ import {IconExternalLink} from '@tabler/icons';
 import {Allotment} from "allotment";
 import "allotment/dist/style.css";
 
-import {newOnionContextProvider} from "./onion_context";
+import {OnionContextProvider} from "./onion_context";
 import {Styledtabs} from "./styledtabs";
 import {demo_PD_description, getDemoPD} from "./demos/demo_pd";
 import {demo_Corr_description, getDemoCorr} from "./demos/demo_corr";
 import {demo_BM_description, getDemoBM} from "./demos/demo_bm";
 import {demo_Editor_description, getDemoEditor} from "./demos/demo_editor";
 import {demo_Welcome_description, Welcome} from "./demos/demo_welcome";
+import {getDemoLE} from "./demos/demo_le";
 
 export function getApp() {
-    const OnionContextProvider = newOnionContextProvider();
     const DemoEditor = getDemoEditor();
     const DemoPD = getDemoPD();
     const DemoCorr = getDemoCorr();
     const DemoBM = getDemoBM();
+    const DemoLE = getDemoLE();
 
     return function App(props) {
         React.useEffect(() => {
@@ -45,6 +46,7 @@ export function getApp() {
                                         Editor</Tabs.Tab>
                                     <Tabs.Tab style={{width: '100%', border: 0}} value="corr">Correspondence</Tabs.Tab>
                                     <Tabs.Tab style={{width: '100%', border: 0}} value="bm">Blended Modeling</Tabs.Tab>
+                                    <Tabs.Tab style={{width: '100%', border: 0}} value="le">List Editor</Tabs.Tab>
                                 </Tabs.List>
                                 <Divider my="md"/>
                                 <div style={{overflow: 'hidden', paddingLeft: '5px'}}>
@@ -91,6 +93,9 @@ export function getApp() {
                                         <Tabs.Panel value="bm" style={{height: '100%'}}>
                                             <DemoBM/>
                                         </Tabs.Panel>
+                                        <Tabs.Panel value="le">
+                                            <DemoLE/>
+                                        </Tabs.Panel>
                                         <Anchor href="https://msdl.uantwerpen.be/git/jexelmans/onioncollab"
                                                 target="_blank"
                                                 style={{position: "absolute", bottom: 8, right: 8, fontWeight: 'bold'}}

+ 0 - 6
src/frontend/demos/demo_editor.tsx

@@ -75,14 +75,10 @@ export const demo_Editor_description =
 export function getDemoEditor() {
     const cs = newVersionedModel({readonly: true});
 
-    const {getModalManualRenderer} = newManualRenderer();
-
     // returns functional react component
     return function () {
         const [csState, setCsState] = React.useState<VersionedModelState>(cs.initialState);
 
-        const [manualRendererState, setManualRendererState] = React.useState<null | ManualRendererProps>(null);
-
         const csReducer = cs.getReducer(setCsState);
 
         const csComponents = cs.getReactComponents(csState, {
@@ -99,8 +95,6 @@ export function getDemoEditor() {
         const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
 
         return (<div style={{minWidth: 1300}}>
-            {getModalManualRenderer(manualRendererState)}
-
             <SimpleGrid cols={3}>
                 <div>
                     <Group position="apart">

+ 188 - 0
src/frontend/demos/demo_le.tsx

@@ -0,0 +1,188 @@
+import * as React from "react";
+import {SimpleGrid, Text, Title, Stack, Center, Group, Space, Image, Button} from "@mantine/core";
+import {IconTrash, IconRowInsertTop, IconRowInsertBottom} from '@tabler/icons';
+
+import {PrimitiveRegistry, PrimitiveDelta} from "onion/primitive_delta";
+import {mockUuid} from "onion/test_helpers";
+import {PrimitiveValue} from "onion/types";
+import {INodeState, IValueState} from "onion/graph_state";
+
+import {newVersionedModel, VersionedModelState} from "../versioned_model/single_model";
+import {newManualRenderer, ManualRendererProps} from "../versioned_model/manual_renderer";
+import {InfoHoverCard} from "../info_hover_card";
+import {OnionContext} from "../onion_context";
+import {useConst} from "../use_const";
+import {Actionblock, Resultblock} from "./blocks";
+import editor from './assets/editor.svg';
+
+export function getDemoLE() {
+    const cs = newVersionedModel({readonly: true});
+
+    let nextVal = 42;
+
+    // returns functional react component
+    return function () {
+        console.log("Rendering DemoLE");
+
+        const onionContext = React.useContext(OnionContext);
+        const [csState, setCsState] = React.useState<VersionedModelState>(cs.initialState);
+        const csReducer = cs.getReducer(setCsState);
+
+        const listNodeId = useConst(() => {
+            // The node representing the list. It is also always the last item in the list ("nil").
+            const listNode = onionContext.primitiveRegistry.newNodeCreation(onionContext.generateUUID());
+            // Has a 'head' edge, pointing to the first element in the list, initially itself (indicating the list is empty).
+            const v = csReducer.createAndGotoNewVersion([
+                listNode,
+                onionContext.primitiveRegistry.newEdgeCreation(listNode, "next", listNode),
+                onionContext.primitiveRegistry.newEdgeCreation(listNode, "prev", listNode),
+            ], "createList", cs.initialState.version);
+            return listNode.id.value;
+        });
+
+        const listNode = cs.graphState.nodes.get(listNodeId) as INodeState;
+
+        // Inserts list item after prevNode and before nextNode.
+        function insertBetween(prevNode: INodeState, nextNode: INodeState, val: PrimitiveValue) {
+            if (prevNode.getOutgoingEdges().get("next") !== nextNode 
+             || nextNode.getOutgoingEdges().get("prev") !== prevNode) {
+                throw new Error("Assertion failed");
+            }
+
+            const deltas: PrimitiveDelta[] = [];
+            const toUnexec: (()=>void)[] = [];
+            function addDelta(d: PrimitiveDelta) {
+                deltas.push(d);
+                cs.graphState.exec(d);
+                toUnexec.push(() => cs.graphState.unexec(d));
+            }
+
+            const newItemCreation = onionContext.primitiveRegistry.newNodeCreation(onionContext.generateUUID());
+            addDelta(newItemCreation);
+            const updateNext = prevNode.getDeltasForSetEdge(onionContext.primitiveRegistry, "next", newItemCreation);
+            updateNext.forEach(d => addDelta(d));
+            const updatePrev = nextNode.getDeltasForSetEdge(onionContext.primitiveRegistry, "prev", newItemCreation);
+            updatePrev.forEach(d => addDelta(d));
+
+            addDelta(onionContext.primitiveRegistry.newEdgeCreation(newItemCreation, "value", val));
+            addDelta(onionContext.primitiveRegistry.newEdgeCreation(newItemCreation, "prev", prevNode.creation));
+            addDelta(onionContext.primitiveRegistry.newEdgeCreation(newItemCreation, "next", nextNode.creation));
+
+            // revert graphstate
+            toUnexec.reduceRight((_,d) => {d(); return null;}, null);
+
+            csReducer.createAndGotoNewVersion(deltas, "insert"+JSON.stringify(val));            
+        }
+
+        function insertBefore(node: INodeState, val: PrimitiveValue) {
+            const prevNode = node.getOutgoingEdges().get("prev") as INodeState;
+            return insertBetween(prevNode, node, val);
+        }
+
+        function insertAfter(node: INodeState, val: PrimitiveValue) {
+            const nextNode = node.getOutgoingEdges().get("next") as INodeState;
+            return insertBetween(node, nextNode, val);
+        }
+
+        function deleteItem(node: INodeState) {
+            const prevNode = node.getOutgoingEdges().get("prev") as INodeState;
+            const nextNode = node.getOutgoingEdges().get("next") as INodeState;
+
+            const deltas: PrimitiveDelta[] = [];
+            const toUnexec: (()=>void)[] = [];
+            function addDelta(d: PrimitiveDelta) {
+                deltas.push(d);
+                cs.graphState.exec(d);
+                toUnexec.push(() => cs.graphState.unexec(d));
+            }
+
+            prevNode.getDeltasForSetEdge(onionContext.primitiveRegistry, "next", nextNode.creation).forEach(addDelta);
+            nextNode.getDeltasForSetEdge(onionContext.primitiveRegistry, "prev", prevNode.creation).forEach(addDelta);
+            node.getDeltasForDelete(onionContext.primitiveRegistry).forEach(addDelta);
+
+            toUnexec.reduceRight((_,d)=>{d(); return null;}, null);
+
+            csReducer.createAndGotoNewVersion(deltas, "deleteItem");            
+        }
+
+        const csComponents = cs.getReactComponents(csState, {
+            onUserEdit: (deltas, description) => {
+                const newVersion = csReducer.createAndGotoNewVersion(deltas, description);
+            },
+            onUndoClicked: csReducer.undo,
+            onRedoClicked: csReducer.redo,
+            onVersionClicked: csReducer.gotoVersion,
+        });
+
+        const deltaTabs = ["dependencyL1", "dependencyL0", "history"];
+
+        const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
+
+        function renderList(head: INodeState, stopAt: INodeState) {
+            const value = head.getOutgoingEdges().get("value");
+            const itemText = value === undefined ? "Start of list" : (value as IValueState).value;
+            const nextItem = head.getOutgoingEdges().get("next") as INodeState;
+            return <>
+                <div style={{border:"solid", borderWidth:"1px"}}>
+                    {itemText}
+                    <Button onClick={() => insertBefore(head, nextVal++)} compact leftIcon={<IconRowInsertBottom/>}>Insert before</Button>
+                    <Button onClick={() => insertAfter(head, nextVal++)} compact leftIcon={<IconRowInsertTop/>}>Insert after</Button>
+                    {value === undefined ? <></> : <Button compact onClick={() => deleteItem(head)} leftIcon={<IconTrash />}>Delete</Button>}
+                </div>
+                {nextItem === stopAt ? <></> : renderList(nextItem, stopAt)}
+            </>;
+        }
+
+        return <div style={{minWidth: 1300}}>
+            <SimpleGrid cols={3}>
+                <div>
+                    <Group position="apart">
+                        <Title order={4}>List Editor</Title>
+                    </Group>
+                    <Space h="48px"/>
+                    <Stack>
+                        {
+                        listNode === undefined ? <>There exists no list (did you undo the list creation?)</>
+                        : <>
+                        <Stack>
+                            {renderList(listNode, listNode)}
+                        </Stack>
+                        </>
+                        }
+                        <Center>
+                            {csComponents.undoRedoButtons}
+                            <Space w="sm"/>
+                            <InfoHoverCard>
+                                {undoButtonHelpText}
+                            </InfoHoverCard>
+                        </Center>
+                    </Stack>
+                </div>
+                <div>
+                    <Group position="center">
+                        <Title order={4}>Deltas</Title>
+                    </Group>
+                    <Space h="sm"/>
+                    {csComponents.makeTabs("dependencyL1", deltaTabs)}
+                </div>
+                <div>
+                    <Title order={4}>State Graph (read-only)</Title>
+                    <Space h="48px"/>
+                    {csComponents.graphStateComponent}
+                    Nodes:
+                    <ul>
+                        {csState.graph.nodes.map(n => {
+                            return <li key={n.id}>{n.id}</li>;
+                        })}
+                    </ul>
+                    Edges:
+                    <ul>
+                        {csState.graph.links.map(e => {
+                            return <li key={e.source.id+"-"+e.label+"->"+e.target.id}>{e.source.id} ---{e.label}--&gt; {e.target.id}</li>
+                        })}
+                    </ul>
+                </div>
+            </SimpleGrid>
+        </div>;
+    }
+}

+ 1 - 1
src/frontend/demos/demo_pd.tsx

@@ -184,7 +184,7 @@ export function getDemoPD() {
                     </div>
                     <div>
                         <Text>Deltas (L0)</Text>
-                        {components.depGraphL0Component}
+                        {components.deltaGraphL0Component}
                     </div>
                 </SimpleGrid>
                 <Space h="md"/>

+ 10 - 11
src/frontend/onion_context.tsx

@@ -3,6 +3,7 @@ import * as React from "react";
 import {PrimitiveRegistry} from "onion/primitive_delta";
 import {UUID} from "onion/types";
 import {mockUuid} from "onion/test_helpers";
+import {useConst} from "./use_const";
 
 export interface OnionContextType {
   primitiveRegistry: PrimitiveRegistry;
@@ -16,15 +17,13 @@ export const OnionContext = React.createContext<OnionContextType>({
   }
 });
 
-export function newOnionContextProvider() {
-  const generateUUID = mockUuid();
-  const primitiveRegistry = new PrimitiveRegistry();
+export function OnionContextProvider({children}) {
+  const generateUUID = useConst(() => mockUuid());
+  const primitiveRegistry = useConst(() => new PrimitiveRegistry());
 
-  return function({children}) {
-    return (
-      <OnionContext.Provider value={{generateUUID, primitiveRegistry}}>
-        {children}
-      </OnionContext.Provider>
-    );
-  }
-}
+  return (
+    <OnionContext.Provider value={{generateUUID, primitiveRegistry}}>
+      {children}
+    </OnionContext.Provider>
+  );
+}

+ 11 - 0
src/frontend/use_const.ts

@@ -0,0 +1,11 @@
+import * as React from "react";
+
+// Why doesn't React have this functionality built-in? (and implemented more efficiently than hacking with useRef)
+export function useConst<T>(initialCb: ()=>T): T {
+  const ref = React.useRef<T>();
+  if (ref.current === undefined) {
+    const initialValue = initialCb();
+    ref.current = initialCb();
+  }
+  return ref.current!;
+}

+ 1 - 1
src/frontend/versioned_model/help_text.tsx

@@ -48,7 +48,7 @@ export const rountangleEditor = <>
   </Mantine.Text>
 </>;
 
-export const depGraph = <>
+export const deltaGraph = <>
   <Mantine.Divider label="Legend" labelPosition="center"/>
   <Mantine.Text>
     <b>Node</b>: Delta<br/>

+ 9 - 6
src/frontend/versioned_model/single_model.tsx

@@ -204,11 +204,11 @@ export function newVersionedModel({readonly}) {
           />}
     </InfoHoverCardOverlay>;
 
-    const depGraphL1Component = <InfoHoverCardOverlay contents={helpText.depGraph}>
+    const deltaGraphL1Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
       <D3Graph graph={state.deltaGraphL1} forces={defaultGraphForces} />
     </InfoHoverCardOverlay>;
 
-    const depGraphL0Component = <InfoHoverCardOverlay contents={helpText.depGraph}>
+    const deltaGraphL0Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
       <D3Graph graph={state.deltaGraphL0} forces={defaultGraphForces} />
     </InfoHoverCardOverlay>;
 
@@ -297,10 +297,10 @@ export function newVersionedModel({readonly}) {
           {rountangleEditor}
         </Mantine.Tabs.Panel>
         <Mantine.Tabs.Panel value="dependencyL1">
-          {depGraphL1Component}
+          {deltaGraphL1Component}
         </Mantine.Tabs.Panel>
         <Mantine.Tabs.Panel value="dependencyL0">
-          {depGraphL0Component}
+          {deltaGraphL0Component}
         </Mantine.Tabs.Panel>
         <Mantine.Tabs.Panel value="history">
           {historyComponent}
@@ -308,11 +308,12 @@ export function newVersionedModel({readonly}) {
       </Mantine.Tabs>;
     }
 
+    // React components:
     return {
       graphStateComponent,
       rountangleEditor,
-      depGraphL1Component,
-      depGraphL0Component,
+      deltaGraphL1Component,
+      deltaGraphL0Component,
       historyComponent,
       undoButton,
       redoButton,
@@ -322,8 +323,10 @@ export function newVersionedModel({readonly}) {
     };
   }
 
+  // State, reducers, etc.
   return {
     initialState,
+    graphState,
     getCurrentVersion,
     getReducer,
     getReactComponents,

+ 0 - 124
src/onion/legacy/disabled_graph_state.ts

@@ -1,124 +0,0 @@
-// import {GraphStateListener, NodeState, ValueState} from "./graph_state";
-// import {NodeCreation} from "./primitive_delta";
-// import {PrimitiveValue} from "./types";
-
-// export class Node {
-//   uuid: PrimitiveValue;
-//   // currently incoming edges
-//   incoming: Edge[] = [];
-//   // currently outgoing edges
-//   outgoing: Map<string, Edge> = new Map();
-
-//   // to get access to the Deltas that created the node, and connected edges to the node.
-//   nodeState: NodeState;
-
-//   constructor(uuid: PrimitiveValue, nodeState: NodeState) {
-//     this.uuid = uuid;
-//     this.nodeState = nodeState;
-//   }
-// }
-
-// export class Value {
-//   value: PrimitiveValue;
-//   incoming: Edge[] = [];
-
-//   constructor(value: PrimitiveValue) {
-//     this.value = value;
-//   }
-// }
-
-// export class Edge {
-//   source: Node;
-//   label: string;
-//   target: Node | Value;
-
-//   constructor(source: Node, label: string, target: Node | Value) {
-//     this.source = source;
-//     this.label = label;
-//     this.target = target;
-//   }
-// }
-
-// // An implementation of GraphStateListener that builds an in-memory graph.
-// // Useful for tests.
-// // Most operations are O(log(n)), some O(n).
-// // The frontend uses D3GraphStateUpdater instead.
-// export class GraphState implements GraphStateListener {
-//   nodes: Map<PrimitiveValue, Node> = new Map();
-//   values: Map<PrimitiveValue, Value> = new Map();
-//   edges: Edge[] = [];
-
-//   createNode(ns: NodeState) {
-//     const uuid = ns.creation.id.value;
-//     this.nodes.set(uuid, new Node(uuid, ns));
-//   }
-//   createValue(vs: ValueState) {
-//     const value = vs.value;
-//     this.values.set(value, new Value(value));
-//   }
-//   private getNode(id: PrimitiveValue): Node {
-//     const node = this.nodes.get(id);
-//     if (node === undefined) {
-//       throw new Error("Non-existing node: " + JSON.stringify(id));
-//     }
-//     return node;
-//   }
-//   private getValue(value: PrimitiveValue): Value {
-//     const v = this.values.get(value);
-//     if (v === undefined) {
-//       throw new Error("Non-existing value: " + JSON.stringify(value));
-//     }
-//     return v;
-//   }
-//   private assertNoOutgoing(sourceNode: Node, label: string) {
-//     if (sourceNode.outgoing.has(label)) {
-//       throw new Error("Source (already) has outgoing edge with label " + label);
-//     }
-//   }
-//   deleteNode(id: PrimitiveValue) {
-//     const node = this.getNode(id);
-//     if (node.incoming.length !== 0) {
-//       throw new Error("Cannot delete node that has non-zero number of incoming edges");
-//     }
-//     if (node.outgoing.size !== 0) {
-//       throw new Error("Cannot delete node that has non-zero number of outgoing edges");
-//     }
-//     this.nodes.delete(id);
-//   }
-//   deleteValue(v: PrimitiveValue) {
-//     const value = this.getValue(v);
-//     if (value.incoming.length !== 0) {
-//       throw new Error("Cannot delete value that has non-zero number of incoming edges");
-//     }
-//     this.values.delete(v);
-//   }
-//   createLinkToNode(sourceId: PrimitiveValue, label: string, targetId: PrimitiveValue) {
-//     const sourceNode = this.getNode(sourceId);
-//     const targetNode = this.getNode(targetId);
-//     this.assertNoOutgoing(sourceNode, label);
-//     const edge = new Edge(sourceNode, label, targetNode);
-//     sourceNode.outgoing.set(label, edge);
-//     targetNode.incoming.push(edge);
-//     this.edges.push(edge);
-//   }
-//   createLinkToValue(sourceId: PrimitiveValue, label: string, targetValue: PrimitiveValue) {
-//     const sourceNode = this.getNode(sourceId);
-//     const value = this.getValue(targetValue);
-//     this.assertNoOutgoing(sourceNode, label);
-//     const edge = new Edge(sourceNode, label, value);
-//     sourceNode.outgoing.set(label, edge);
-//     value.incoming.push(edge);
-//     this.edges.push(edge);
-//   }
-//   deleteLink(sourceId: PrimitiveValue, label: string) {
-//     const sourceNode = this.getNode(sourceId);
-//     const edge = sourceNode.outgoing.get(label);
-//     if (edge === undefined) {
-//       throw new Error("Cannot delete non-existing link with source " + JSON.stringify(sourceId) + " and label " + label);
-//     }
-//     sourceNode.outgoing.delete(label);
-//     const incoming = edge.target.incoming;
-//     incoming.splice(incoming.findIndex(e => e === edge), 1);
-//     this.edges.splice(this.edges.findIndex(e => e === edge), 1);
-//   }
-// }

+ 0 - 57
src/onion/legacy/graph_state.test.ts

@@ -1,57 +0,0 @@
-import {GraphState} from "./graph_state"
-import {NodeId, PrimitiveValue, UUID, nodeIdsEqual} from "../types";
-import {mockUuid} from "../test_helpers";
-import {assert} from "../../util/assert";
-
-
-describe("CRUD operations", () => {
-  it("Deleting a node", () => {
-    const s = new GraphState(mockUuid());
-
-    const n1 = s.createNode();
-    const n2 = s.createNode();
-
-    s.setEdge(n1, 'edge', n2);
-    s.setEdge(n2, 'x', 42);
-    s.setEdge(n2, 'y', 420);
-    s.setEdge(42, 'z', n1);
-
-    assert(s.getEdges().length === 4, "Expected 4 edges.");
-
-    s.deleteNode(n2);
-    assert(s.getEdges().length === 1, "Expected incoming and outgoing edges of n2 to be deleted.")
-  });
-
-  it("Updating an edge", () => {
-    const s = new GraphState(mockUuid());
-
-    const n1 = s.createNode();
-    const n2 = s.createNode();
-
-    s.setEdge(n1, 'edge', n2);
-    s.setEdge(n2, 'x', 42);
-    s.setEdge(n2, 'y', 420);
-
-    assert(s.readEdge(n2, 'x') === 42, "expected n2.x === 42");
-
-    s.setEdge(n2, 'x', 43);
-
-    assert(s.readEdge(n2, 'x') === 43, "expected n2.x === 43");
-
-    s.deleteEdge(n1, 'edge');
-
-    assert(s.readEdge(n1, 'edge') === undefined, "edge should have been deleted.");
-
-    assert(s.readEdge(n2, 'a') === undefined, "edge should not exist.");
-  });
-
-  it("Delete non-existing node", () => {
-    const s = new GraphState(mockUuid());
-    s.deleteNode(new UUID('non-existent-uuid'));
-  });
-
-  it("Read non-existing edge", () => {
-    const s = new GraphState(mockUuid());
-    assert(s.readEdge(new UUID('non-existent-uuid'), 'x') === undefined, "edge should not exist.");
-  })
-})

+ 0 - 190
src/onion/legacy/graph_state.ts

@@ -1,190 +0,0 @@
-import {
-  NodeId,
-  PrimitiveValue,
-  UUID,
-  nodeIdsEqual,
-} from "../types";
-
-// In- and outgoing edges of a node.
-// This is the only place where edges are recorded.
-// Every edge corresponds to one entry in the source's 'outgoing', and one entry in the target's 'incoming'.
-class Node {
-  // creation: NodeCreation;
-  outgoing: Map<string, NodeId>; // key: edge label, value: target id.
-  incoming: Array<{label: string, srcId: NodeId}>;
-
-  constructor(/*creation: NodeCreation*/) {
-    //this.creation = creation;
-    this.outgoing = new Map();
-    this.incoming = [];
-  }
-}
-
-// Helper class.
-// Abstracts away the fact that we use 2 maps for our nodes: one for our 'ordinary' nodes, and one for our value nodes.
-class NodeMap {
-  // ordinary nodes: they are created and deleted, and identified by a UUID
-  ordinary: Map<PrimitiveValue, Node>;
-
-  // value nodes: we pretend that they always already exist, and are identified by a PrimitiveValue
-  values: Map<PrimitiveValue, Node>;
-
-  constructor() {
-    this.ordinary = new Map();
-    this.values = new Map();
-  }
-
-  // get a node by its ID. if node doesn't exist, returns undefined.
-  getOptional(id: NodeId): Node | undefined {
-    if (id instanceof UUID) {
-      // ordinary node: can only get it if it actually exists,
-      // i.e., it has already been created and has not yet been deleted.
-      return this.ordinary.get(id.value); // may return undefined
-    }
-    else {
-      // value node: implicitly create it if it doesn't exist yet,
-      // pretending that it's "always already there"
-      const valueNode = this.values.get(id);
-      if (valueNode !== undefined) {
-        return valueNode;
-      } else {
-        // auto-construct non-existing value node
-        const valueNode = new Node();
-        this.values.set(id, valueNode);
-        return valueNode;
-      }
-    }
-  }
-
-  // same as get, but raises error when not found
-  getOrThrow(id: NodeId): Node {
-    const node = this.getOptional(id);
-    if (node === undefined) {
-      throw Error("node not found");
-    }
-    return node;
-  }
-
-  // create a new ordinary node
-  create(id: UUID): Node {
-    const node = new Node();
-    this.ordinary.set(id.value, node);
-    return node;
-  }
-
-  // delete an ordinary node
-  // Idempotent.
-  delete(id: UUID) {
-    this.ordinary.delete(id.value);
-  }
-}
-
-type UUIDCallbackType = () => UUID;
-
-export class GraphState {
-  nodes: NodeMap;
-  uuidCallback: UUIDCallbackType;
-
-  // creations: Map<PrimitiveValue, NodeCreation>;
-
-  constructor(uuidCallback: UUIDCallbackType) {
-    this.nodes = new NodeMap();
-    this.uuidCallback = uuidCallback;
-  }
-
-  // Create a new node.
-  createNode(): UUID {
-    const uuid = this.uuidCallback();
-    this.nodes.create(uuid);
-    // const creation = new NodeCreation(uuid);
-    // this.creations.set(uuid.value, creation);
-    return uuid;
-  }
-
-  // Delete node and delete all of its outgoing + incoming edges.
-  // Does nothing when given uuid does not exist./
-  // Idempotent.
-  deleteNode(uuid: UUID) {
-    const node = this.nodes.getOptional(uuid);
-    if (node !== undefined) {
-      // delete outgoing edges
-      for (const [label, tgtId] of node.outgoing.entries()) {
-        const tgtNode = this.nodes.getOrThrow(tgtId);
-        // remove edge from tgtNode.incoming
-        const i = this.lookupIncoming(tgtNode, label, uuid);
-        tgtNode.incoming.splice(i, 1);
-      }
-      // delete incoming edges
-      for (const {label, srcId} of node.incoming) {
-        const srcNode = this.nodes.getOrThrow(srcId);
-        // remove edge from srcNode.outgoing
-        srcNode.outgoing.delete(label);
-      }
-      // delete node itself
-      this.nodes.delete(uuid);      
-    }
-  }
-
-  // Create or update a node's outgoing edge to point to a node
-  // Idempotent.
-  setEdge(srcId: NodeId, label: string, tgtId: NodeId) {
-    // gotta remove the existing edge first, if it exists
-    this.deleteEdge(srcId, label);
-
-    const srcNode = this.nodes.getOrThrow(srcId);
-    srcNode.outgoing.set(label, tgtId);
-    const tgtNode = this.nodes.getOrThrow(tgtId);
-    tgtNode.incoming.push({label, srcId});
-  }
-
-  // Delete an edge.
-  // Idempotent.
-  deleteEdge(srcId: NodeId, label: string) {
-    const srcNode = this.nodes.getOrThrow(srcId);
-    const existingTgtId = srcNode.outgoing.get(label);
-    if (existingTgtId !== undefined) {
-      // remove the respective entry in the existingTgtNode's 'incoming' array:
-      const existingTgtNode = this.nodes.getOrThrow(existingTgtId);
-      const i = this.lookupIncoming(existingTgtNode, label, srcId);
-      existingTgtNode.incoming.splice(i, 1); // remove from array
-    }
-    srcNode.outgoing.delete(label);
-  }
-
-  // In a node's array of incoming edges, search for {label, srcId}, and return the position in the array.
-  private lookupIncoming(node: Node, label: string, srcId: NodeId): number {
-    for (const [i, {label: l, srcId: s}] of node.incoming.entries()) {
-      if (l === label && nodeIdsEqual(s, srcId)) {
-        return i;
-      }
-    }
-    throw new Error("Not found!");
-  }
-
-  // Read a node's outgoing edge with given label.
-  // If no such edge exists, returns undefined.
-  readEdge(srcId: NodeId, label: string): NodeId | undefined {
-    const srcNode = this.nodes.getOptional(srcId);
-    if (srcNode !== undefined) {
-      return srcNode.outgoing.get(label);
-    }
-  }
-
-  // Get all the edges in the graph. Slow. For debugging purposes.
-  getEdges(): Array<[NodeId, NodeId, string]> {
-    const result: Array<[NodeId, NodeId, string]> = [];
-    // get all outgoing edges of ordinary nodes
-    for (const [srcId, srcNode] of this.nodes.ordinary.entries()) {
-      for (const [label, tgtId] of srcNode.outgoing) {
-        result.push([new UUID(srcId), tgtId, label]);
-      }
-    }
-    // get all outgoing edges of value nodes
-    for (const [srcId, srcNode] of this.nodes.values.entries()) {
-      for (const [label, tgtId] of srcNode.outgoing) {
-        result.push([srcId, tgtId, label]);
-      }
-    }
-    return result;
-  }
-}

+ 3 - 2
src/onion/primitive_delta.ts

@@ -610,9 +610,10 @@ function targetToHash(target: EdgeTargetType): any {
 export class PrimitiveRegistry {
   deltas: Map<string, PrimitiveDelta> = new Map();
 
-  private createIdempotent(hash: Buffer, callback) {
+  // Given the expected hash 
+  private createIdempotent<T extends PrimitiveDelta>(hash: Buffer, callback: () => T): T {
     const base64 = hash.toString('base64');
-    return this.deltas.get(base64) || (() => {
+    return this.deltas.get(base64) as T || (() => {
       const delta = callback();
       this.deltas.set(base64, delta);
       return delta;

+ 0 - 9
src/onion/types.ts

@@ -15,12 +15,3 @@ export class UUID {
     return "UUID{" + inspect(this.value, options) + "}"
   }
 }
-
-export type NodeId = PrimitiveValue | UUID;
-
-export function nodeIdsEqual(a: NodeId, b: NodeId) {
-  if (a === b) return true;
-  if (a instanceof UUID && b instanceof UUID) {
-    return a.value === b.value;
-  }
-}