changeset 18:4bf74032e2e8

merge
author drewp@bigasterisk.com
date Thu, 12 Dec 2019 22:57:14 -0800
parents 94629c39681c (current diff) 3f3c27ed27eb (diff)
children 23fa6402c728
files package.json src/rdf_types.ts tasks.py tsconfig.json
diffstat 12 files changed, 510 insertions(+), 311 deletions(-) [+]
line wrap: on
line diff
--- a/index.html	Thu Dec 12 22:52:57 2019 -0800
+++ b/index.html	Thu Dec 12 22:57:14 2019 -0800
@@ -4,6 +4,6 @@
         <h1>streamed-graph demo</h1>
 
         <script src="./build/streamed-graph.bundle.js"></script>
-        <streamed-graph url="http://bang5:9075/graph/events"></streamed-graph>
+        <streamed-graph url="http://bang5:9075/graph/events" expanded="true"></streamed-graph>
     </body>
 </html>
\ No newline at end of file
--- a/jasmine.json	Thu Dec 12 22:52:57 2019 -0800
+++ b/jasmine.json	Thu Dec 12 22:57:14 2019 -0800
@@ -1,5 +1,7 @@
 {
-    "spec_dir": "src",
-    "spec_files": ["**/*_test.ts"],
+    "spec_dir": "build",
+    "spec_files": [
+      "test.bundle.js"
+    ],
     "oneFailurePerSpec": false
   }
\ No newline at end of file
--- a/src/graph_view.ts	Thu Dec 12 22:52:57 2019 -0800
+++ b/src/graph_view.ts	Thu Dec 12 22:57:14 2019 -0800
@@ -1,208 +1,238 @@
 // from /my/site/homepage/www/rdf/browse/graphView.js
 
-/// <reference types="./n3.d.ts">
+//reference types="./n3.d.ts">
 
-import { html } from 'lit-html';
+import { html, TemplateResult } from 'lit-html';
 import { SuffixLabels } from './suffixLabels';
 
-import { NamedNode, N3Store } from 'n3';
-
-import ns from 'n3/src/IRIs';
-const {rdf} = ns;
+import { Quad, Term, NamedNode, N3Store } from '../node_modules/@types/n3/index';
+import { DataFactory, Util } from 'n3';
+const { namedNode } = DataFactory;
+// import ns from 'n3/src/IRIs';
+// const { rdf } = ns;
 
-const groupByRdfType = (graph: N3Store) => {
-  const rdfType = new NamedNode(rdf.type);
-  const byType: Map<NamedNode, Set<NamedNode>> = new Map(); // type : subjs
-  const untyped = new Set(); // subjs
-  graph.getQuads({}, (q) => {
-    let subjType = null;
-    graph.getQuads({
-      subject: q.subject,
-      predicate: rdfType
-    },
-      (q2) => { subjType = q2.object; });
-    if (subjType) {
-      subjType = subjType.toString();
+type TypeToSubjs = Map<NamedNode, Set<NamedNode>>;
+function groupByRdfType(graph: N3Store): { byType: TypeToSubjs, untyped: Set<NamedNode> } {
+  const rdfType = namedNode('rdf:type');
+  const byType: TypeToSubjs = new Map(); // type : subjs
+  const untyped: Set<NamedNode> = new Set(); // subjs
+  graph.forEach((q) => {
+    let subjType: NamedNode | null = null;
+
+    graph.forObjects((o: Quad) => {
+      if (Util.isNamedNode(o.object)) {
+        subjType = o.object as NamedNode;
+      }
+    }, q.subject, rdfType, null);
+
+    if (subjType !== null) {
       if (!byType.has(subjType)) {
         byType.set(subjType, new Set());
       }
-      byType.get(subjType).add(q.subject.toString());
+      (byType.get(subjType) as Set<NamedNode>).add(q.subject as NamedNode);
     } else {
-      untyped.add(q.subject.toString());
+      untyped.add(q.subject as NamedNode);
     }
-
-  });
+  }, null, null, null, null);
   return { byType: byType, untyped: untyped };
-};
+}
 
-const graphView = (graph: N3Store) => {
-  const labels = new SuffixLabels();
-  graph.getQuads({}, (q) => {
-    if (q.subject.interfaceName == "NamedNode") { labels.planDisplayForNode(q.subject); }
-    if (q.predicate.interfaceName == "NamedNode") { labels.planDisplayForNode(q.predicate); }
-    if (q.object.interfaceName == "NamedNode") { labels.planDisplayForNode(q.object); }
-    if (q.object.interfaceName == "Literal" && q.object.datatype) { labels.planDisplayForNode(new NamedNode(q.object.datatype)); }
-  });
 
-  const rdfNode = (n) => {
-    if (n.interfaceName == "Literal") {
+class NodeDisplay {
+  labels: SuffixLabels;
+  constructor(labels: SuffixLabels) {
+    this.labels = labels;
+  }
+  getHtml(n: Term): TemplateResult {
+    if (n.termType == "Literal") {
       let dtPart: any = "";
       if (n.datatype) {
         dtPart = html`
-        ^^<span class="literalType">
-          ${rdfNode(new NamedNode(n.datatype))}
-        </span>`;
+            ^^<span class="literalType">
+              ${this.getHtml(n.datatype)}
+            </span>`;
       }
-      return html`<span class="literal">${n.nominalValue}${dtPart}</span>`;
-    }
-    if (n.interfaceName == "NamedNode") {
-      let dn = labels.getLabelForNode(n);
-      if (dn.match(/XMLSchema#.*/)) { dn = dn.replace('XMLSchema#', 'xsd:'); }
-      if (dn.match(/rdf-schema#.*/)) { dn = dn.replace('rdf-schema#', 'rdfs:'); }
-      return html`<a class="graphUri" href="${n.toString()}">${dn}</a>`;
+      return html`<span class="literal">${n.value}${dtPart}</span>`;
     }
 
-    return html`[${n.interfaceName} ${n.toNT()}]`;
+    if (n.termType  == "NamedNode") {
+      let dn: string | undefined = this.labels.getLabelForNode(n.value);
+      if (dn === undefined) {
+        throw new Error(`dn=${dn}`);
+      }
+      if (dn!.match(/XMLSchema#.*/)) {
+        dn = dn!.replace('XMLSchema#', 'xsd:');
+      }
+      if (dn!.match(/rdf-schema#.*/)) {
+        dn = dn!.replace('rdf-schema#', 'rdfs:');
+      }
+      return html`<a class="graphUri" href="${n.value}">${dn}</a>`;
+    }
+
+    return html`[${n.termType} ${n.value}]`;
+  }
+}
+
+export class GraphView {
+  url: string;
+  graph: N3Store;
+  nodeDisplay: NodeDisplay;
+  constructor(url: string, graph: N3Store) {
+    this.url = url;
+    this.graph = graph;
+
+    const labels = new SuffixLabels();
+    this._addLabelsForAllTerms(labels);
+    this.nodeDisplay = new NodeDisplay(labels);
   }
 
-  const objBlock = (obj) => {
-    return html`
-        <div class="object">
-          ${rdfNode(obj)} <!-- indicate what source or graph said this stmt -->
-        </div>
-    `;
-  };
+  _addLabelsForAllTerms(labels: SuffixLabels) {
+    return this.graph.forEach((q: Quad) => {
+      if (q.subject.termType === "NamedNode") { labels.planDisplayForNode(q.subject); }
+      if (q.predicate.termType === "NamedNode") { labels.planDisplayForNode(q.predicate); }
+      if (q.object.termType === "NamedNode") { labels.planDisplayForNode(q.object); }
+      if (q.object.termType === "Literal" && q.object.datatype) {
+        labels.planDisplayForNode(q.object.datatype);
+      }
+    }, null, null, null, null);
+  }
 
-  /// bunch of table rows
-  const predBlock = (subj, pred) => {
-    const objsSet = new Set();
-    graph.getQuads({ subject: subj, predicate: pred }, (q) => {
-
-      if (q.object.length) {
-        console.log(q.object)
-      }
-      objsSet.add(q.object);
-    });
-    const objs = Array.from(objsSet.values());
-    objs.sort();
+  _subjBlock(subj: NamedNode) {
+    const predsSet: Set<NamedNode> = new Set();
+    this.graph.forEach((q: Quad) => {
+      predsSet.add(q.predicate as NamedNode);
+    }, subj, null, null, null);
+    const preds = Array.from(predsSet.values());
+    preds.sort();
     return html`
-      <div class="predicate">${rdfNode(pred)}
+      <div class="subject">${this.nodeDisplay.getHtml(subj)}
+        <!-- todo: special section for uri/type-and-icon/label/comment -->
         <div>
-          ${objs.map(objBlock)}
+          ${preds.map((p) => { return this._predBlock(subj, p); })}
         </div>
       </div>
     `;
-  };
+  }
 
-  const { byType, untyped } = groupByRdfType(graph);
-  const typedSubjs = Array.from(byType.keys());
-  typedSubjs.sort();
-
-  const untypedSubjs = Array.from(untyped.values());
-  untypedSubjs.sort();
+  _objBlock(obj: Term) {
+    return html`
+      <div class="object">
+        ${this.nodeDisplay.getHtml(obj)} <!-- indicate what source or graph said this stmt -->
+      </div>
+    `;
+  }
 
-  const subjBlock = (subj) => {
-    const subjNode = new NamedNode(subj);
-    const predsSet = new Set();
-    graph.getQuads({ subject: subjNode }, (q) => {
-      predsSet.add(q.predicate);
-    });
-    const preds = Array.from(predsSet.values());
-    preds.sort();
+  _predBlock(subj: NamedNode, pred: NamedNode) {
+    const objsSet = new Set();
+    this.graph.forEach((q: Quad) => {
+      objsSet.add(q.object);
+    }, subj, pred, null, null);
+    const objs = Array.from(objsSet.values());
+    objs.sort();
     return html`
-      <div class="subject">${rdfNode(subjNode)}
-        <!-- todo: special section for uri/type-and-icon/label/comment -->
+      <div class="predicate">${this.nodeDisplay.getHtml(pred)}
         <div>
-          ${preds.map((p) => { return predBlock(subjNode, p); })}
+          ${objs.map(this._objBlock.bind(this))}
         </div>
       </div>
     `;
-  };
-  const byTypeBlock = (typeUri) => {
-    const subjs = Array.from(byType.get(typeUri));
-    subjs.sort();
+  }
+
 
-    const graphCells = new Map(); // [subj, pred] : objs
-    const preds = new Set();
+  //   const byTypeBlock = (typeUri) => {
+  //     const subjs = Array.from(byType.get(typeUri));
+  //     subjs.sort();
+
+  //     const graphCells = new Map(); // [subj, pred] : objs
+  //     const preds = new Set();
 
-    subjs.forEach((subj) => {
-      graph.getQuads({ subject: new NamedNode(subj) }, (q) => {
-        preds.add(q.predicate.toString());
-        const cellKey = subj + '|||' + q.predicate.toString();
-        if (!graphCells.has(cellKey)) {
-          graphCells.set(cellKey, new Set());
-        }
-        graphCells.get(cellKey).add(q.object);
-      });
-    });
-    const predsList = Array.from(preds);
-    predsList.splice(predsList.indexOf('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), 1);
-    // also pull out label, which should be used on 1st column
-    predsList.sort();
+  //     subjs.forEach((subj) => {
+  //       graph.getQuads({ subject: new NamedNode(subj) }, (q) => {
+  //         preds.add(q.predicate.toString());
+  //         const cellKey = subj + '|||' + q.predicate.toString();
+  //         if (!graphCells.has(cellKey)) {
+  //           graphCells.set(cellKey, new Set());
+  //         }
+  //         graphCells.get(cellKey).add(q.object);
+  //       });
+  //     });
+  //     const predsList = Array.from(preds);
+  //     predsList.splice(predsList.indexOf('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), 1);
+  //     // also pull out label, which should be used on 1st column
+  //     predsList.sort();
+
+  //     const thead = () => {
+  //       const predColumnHead = (pred) => {
+  //         return html`<th>${rdfNode(new NamedNode(pred))}</th>`;
+  //       };
+  //       return html`
+  //               <thead>
+  //                 <tr>
+  //                   <th></th>
+  //                   ${predsList.map(predColumnHead)}
+  //                 </tr>
+  //               </thead>`;
+  //     };
 
-    const thead = () => {
-      const predColumnHead = (pred) => {
-        return html`<th>${rdfNode(new NamedNode(pred))}</th>`;
-      };
-      return html`
-              <thead>
-                <tr>
-                  <th></th>
-                  ${predsList.map(predColumnHead)}
-                </tr>
-              </thead>`;
-    };
+  //     const instanceRow = (subj) => {
+  //       const cell = (pred) => {
+  //         const objs = graphCells.get(subj + '|||' + pred);
+  //         if (!objs) {
+  //           return html`<td></td>`;
+  //         }
+  //         const objsList = Array.from(objs);
+  //         objsList.sort();
+  //         const draw = (obj) => {
+  //           return html`<div>${rdfNode(obj)}</div>`
+  //         };
+  //         return html`<td>${objsList.map(draw)}</td>`;
+  //       };
 
-    const instanceRow = (subj) => {
-      const cell = (pred) => {
-        const objs = graphCells.get(subj + '|||' + pred);
-        if (!objs) {
-          return html`<td></td>`;
-        }
-        const objsList = Array.from(objs);
-        objsList.sort();
-        const draw = (obj) => {
-          return html`<div>${rdfNode(obj)}</div>`
-        };
-        return html`<td>${objsList.map(draw)}</td>`;
-      };
+  //       return html`
+  //               <tr>
+  //                 <td>${rdfNode(new NamedNode(subj))}</td>
+  //                 ${predsList.map(cell)}
+  //               </tr>
+  //             `;
+  //     };
 
-      return html`
-              <tr>
-                <td>${rdfNode(new NamedNode(subj))}</td>
-                ${predsList.map(cell)}
-              </tr>
-            `;
-    };
+  //     return html`
+  //           <div>[icon] ${rdfNode(new NamedNode(typeUri))} resources</div>
+  // <div class="typeBlockScroll">
+  //           <table class="typeBlock">
+  //             ${thead()}
+  //             ${subjs.map(instanceRow)}
+  //           </table>
+  // </div>
+  //         `;
+  //   };
+
+  makeTemplate(): TemplateResult {
+
+    const { byType, untyped } = groupByRdfType(this.graph);
+    const typedSubjs = Array.from(byType.keys());
+    typedSubjs.sort();
+
+    const untypedSubjs = Array.from(untyped.values());
+    untypedSubjs.sort();
 
     return html`
-          <div>[icon] ${rdfNode(new NamedNode(typeUri))} resources</div>
-<div class="typeBlockScroll">
-          <table class="typeBlock">
-            ${thead()}
-            ${subjs.map(instanceRow)}
-          </table>
-</div>
-        `;
-  };
-
-  return html`
-      <link rel="stylesheet" href="/rdf/browse/style.css">
+        <link rel="stylesheet" href="../src/streamed-graph.css">
 
-      <section>
-        <h2>
-          Current graph (<a href="${graph.events.url}">${graph.events.url}</a>)
-        </h2>
-        <div>
-         <!-- todo: graphs and provenance.
-          These statements are all in the
-          <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
-        </div>
-        ${typedSubjs.map(byTypeBlock)}
-        <div class="spoGrid">
-          ${untypedSubjs.map(subjBlock)}
-        </div>
-      </section>
-    `;
+        <section>
+          <h2>
+            Current graph (<a href="${this.url}">${this.url}</a>)
+          </h2>
+          <div>
+           <!-- todo: graphs and provenance.
+            These statements are all in the
+            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
+          </div>
+          {typedSubjs.map(byTypeBlock)}
+          <div class="spoGrid">
+            ${untypedSubjs.map(this._subjBlock.bind(this))}
+          </div>
+        </section>
+      `;
+  }
 }
-export { graphView }
--- a/src/json_ld_quads.ts	Thu Dec 12 22:52:57 2019 -0800
+++ b/src/json_ld_quads.ts	Thu Dec 12 22:57:14 2019 -0800
@@ -12,7 +12,7 @@
 function _emitQuad(
     onQuad: (q: Quad) => void,
     subjNode: NamedNode,
-    pred: string | { '@id': string },
+    pred: string,
     subj: any,
     graphNode: NamedNode) {
     let predNode: NamedNode;
@@ -20,7 +20,7 @@
         predNode = namedNode(rdf.type);
     }
     else {
-        predNode = namedNode(pred['@id']);
+        predNode = namedNode(pred);
     }
     subj[pred as string].forEach(function (obj: any) {
         const objNode = (obj['@id'] ? namedNode(obj['@id']) :
@@ -41,12 +41,14 @@
             (expanded as [object]).forEach(function (g) {
                 var graph = g['@id'];
                 var graphNode = namedNode(graph);
-                g['@graph'].forEach(function (subj) {
+                g['@graph'].forEach(function (subj: { [predOrId: string]: any; }) {
+                    console.log('process subj', subj)
                     const subjNode = namedNode(subj['@id']);
                     for (let pred in subj) {
                         if (pred === '@id') {
                             continue;
-                        } 2
+                        }
+                        console.log('emit with', pred);
                         _emitQuad(onQuad, subjNode, pred, subj, graphNode);
                     }
                 });
--- a/src/rdf_types.ts	Thu Dec 12 22:52:57 2019 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-export type Node = any; // todo
-export type Quad = {
-    subject: Node;
-    predicate: Node;
-    object: Node;
-    graph: Node,
-};
--- a/src/streamed-graph.css	Thu Dec 12 22:52:57 2019 -0800
+++ b/src/streamed-graph.css	Thu Dec 12 22:57:14 2019 -0800
@@ -19,3 +19,112 @@
     display: inline-block;
     padding: 3px;
 }
+
+/* graph view */
+@import url('https://fonts.googleapis.com/css?family=Allerta|Dosis|Jura&display=swap');
+
+body.rdfBrowsePage {
+    background: black;
+    color: white;
+    font-family: 'Allerta', sans-serif;
+    font-size: 12px;
+}
+
+body.rdfBrowsePage  pre {
+    font-family: 'Allerta', sans-serif;
+}
+
+body.rdfBrowsePage  a {
+    color: #b1b1fd;
+    text-shadow: 1px 1px 0px #0400ff94;
+    text-decoration-color: #00007714;
+}
+
+section {
+    border: 1px solid gray;
+    padding: 4px;
+    font-family: 'Allerta', sans-serif;
+}
+.spoGrid {
+    display: flex;
+    flex-direction: column;
+}
+.subject, .predicate {
+    display: flex;
+    align-items: baseline;
+}
+
+.predicate, .object {
+    margin-left: 5px;
+}
+.subject { border-top: 1px solid #2f2f2f; }
+.literal {
+    border: 1px solid gray;
+    border-radius: 9px;
+    padding: 4px;
+    margin: 3px;
+}
+
+.subject > .node { border: 2px solid rgb(68, 141, 68); }
+.literalType {
+    vertical-align: super;
+    font-size: 80%;
+}
+.literal {
+    display: inline-block;
+    font-family: monospace;
+    font-size: 115%;
+}
+.resource {
+    display: inline-block;
+    background: lightblue;
+    border-radius: 6px;
+    padding: 1px 6px;
+    margin: 2px;
+}
+.predicate > a {
+    color: #e49dfb;
+}
+.predicate > a::before {
+    content: "━";
+    font-size: 125%;
+    font-weight: bolder;
+    padding-right: 2px;
+}
+.predicate > a::after {
+    content: "🠪";
+}
+.comment { color: green; }
+
+table.typeBlock {
+    border-collapse: collapse;
+}
+table.typeBlock th {
+    background: #1f1f1f;
+    border: 2px #333333 outset;
+}
+table.typeBlock td {
+    white-space: nowrap;
+    background: #2b2b2b;
+    border: 2px #4a4a4a outset;
+}
+table.typeBlock td .literal {
+    padding-top: 1px;
+    padding-bottom: 1px;
+}
+.typeBlockScroll {
+    max-width: 100%;
+    overflow-x: auto;
+}
+
+/*
+for my pages serving rdf data, not necessarily part of browse/
+*/
+.served-resources {
+    margin-top: 4em;
+    border-top: 1px solid gray;
+    padding-top: 1em;
+}
+.served-resources a {
+    padding-right: 2em;
+}
--- a/src/streamed-graph.ts	Thu Dec 12 22:52:57 2019 -0800
+++ b/src/streamed-graph.ts	Thu Dec 12 22:57:14 2019 -0800
@@ -6,7 +6,7 @@
 import { PolymerElement, html } from '@polymer/polymer';
 import { customElement, property, computed } from '@polymer/decorators';
 import { render } from 'lit-html';
-import { graphView } from './graph_view';
+import { GraphView } from './graph_view';
 import { N3Store } from "n3"
 
 import { StreamedGraphClient } from './streamed_graph_client';
@@ -16,81 +16,84 @@
     @property({ type: String })
     url: string = '';
 
-    @property({ type: Object })
-    graph: {version: number, graph: N3Store};
+    // @property({ type: Object })
+    // graph: {version: number, store!: N3Store};
 
-    @property({ type: Boolean })
-    expanded: Boolean = false;
+    // @property({ type: boolean })
+    // expanded: boolean = false;
 
-    @computed('expanded')
-    get expandAction() {
-        return this.expanded ? '-' : '+';
-    }
+    // @computed('expanded')
+    // expandAction(): string {
+    //     return this.expanded ? '-' : '+';
+    // }
 
-    @property({ type: String })
-    status: String = '';
+    // @property({ type: string })
+    // status: string = '';
 
-    sg: StreamedGraphClient;
-    graphView: Element;
-    graphViewDirty = true;
+    // sg: StreamedGraphClient;
+    // graphView: Element;
+    // graphViewDirty = true;
 
-    static get template() {
-        return html`
-            <link rel="stylesheet" href="../src/streamed-graph.css">
-            <div id="ui">
-                <span class="expander"><button on-click="toggleExpand">{{expandAction}}</button></span>
-                StreamedGraph <a href="{{url}}">[source]</a>:
-                {{status}}
-            </div>
-            <div id="graphView"></div>`;
-    }
+    // static get template() {
+    //     return html`
+    //         <link rel="stylesheet" href="../src/streamed-graph.css">
+    //         <div id="ui">
+    //             <span class="expander"><button on-click="toggleExpand">{{expandAction}}</button></span>
+    //             StreamedGraph <a href="{{url}}">[source]</a>:
+    //             {{status}}
+    //         </div>
+    //         <div id="graphView"></div>`;
+    // }
 
-    ready() {
-        super.ready();
-        this.graph = {version: -1, graph: null};
-        this.graphView = this.shadowRoot.getElementById("graphView");
+    // ready() {
+    //     super.ready();
+    //     this.graph = {version: -1, store: null};
+    //     this.graphView = (this.shadowRoot as ShadowRoot).getElementById("graphView") as Element;
 
-        this._onUrl(this.url); // todo: watch for changes and rebuild
-    }
+    //     this._onUrl(this.url); // todo: watch for changes and rebuild
+    // }
 
-    toggleExpand(ev) {
-        this.expanded = !this.expanded;
-        if (this.expanded) {
-            this.redrawGraph()
-        } else {
-            this.graphViewDirty = false;
-            render(null, this.graphView);
-        }
-    }
+    // toggleExpand(ev) {
+    //     this.expanded = !this.expanded;
+    //     if (this.expanded) {
+    //         this.redrawGraph()
+    //     } else {
+    //         this.graphViewDirty = false;
+    //         render(null, this.graphView);
+    //     }
+    // }
 
-    redrawGraph() {
-        this.graphViewDirty = true;
-        requestAnimationFrame(this._redrawLater.bind(this));
-    }
+    // redrawGraph() {
+    //     this.graphViewDirty = true;
+    //     requestAnimationFrame(this._redrawLater.bind(this));
+    // }
 
-    _onUrl(url) {
-        if (this.sg) { this.sg.close(); }
-        this.sg = new StreamedGraphClient(
-            url,
-            this.onGraphChanged.bind(this),
-            this.set.bind(this, 'status'),
-            [],//window.NS,
-            []
-        );
-    }
+    // _onUrl(url) {
+    //     if (this.sg) { this.sg.close(); }
+    //     this.sg = new StreamedGraphClient(
+    //         url,
+    //         this.onGraphChanged.bind(this),
+    //         this.set.bind(this, 'status'),
+    //         [],//window.NS,
+    //         []
+    //     );
+    // }
 
-    onGraphChanged() {
-        this.graph = { version: this.graph.version + 1, graph: this.sg.store };
-        if (this.expanded) {
-            this.redrawGraph();
-        }
-    }
+    // onGraphChanged() {
+    //     this.graph = { 
+    //         version: this.graph.version + 1, 
+    //         store: this.sg.store
+    //      };
+    //     if (this.expanded) {
+    //         this.redrawGraph();
+    //     }
+    // }
 
-    _redrawLater() {
-        if (!this.graphViewDirty) return;
-        render(graphView(this.graph.graph), this.graphView);
-        this.graphViewDirty = false;
-    }
+    // _redrawLater() {
+    //     if (!this.graphViewDirty) return;
+    //     render(new GraphView(this.url, this.graph.store).makeTemplate(), this.graphView);
+    //     this.graphViewDirty = false;
+    // }
 
 
 }
\ No newline at end of file
--- a/src/streamed_graph_client.ts	Thu Dec 12 22:52:57 2019 -0800
+++ b/src/streamed_graph_client.ts	Thu Dec 12 22:57:14 2019 -0800
@@ -1,13 +1,8 @@
 // from /my/site/homepage/www/rdf/streamed-graph.js
 
-import * as async from "async";
-// import * as jsonld from "jsonld";
-
 import { eachJsonLdQuad } from "./json_ld_quads";
-import { Store } from "n3"
-
-// /// <reference types="eventsource" />
-// const EventSource = window.EventSource;
+import { N3Store } from '../node_modules/@types/n3/index';
+import { Store } from 'n3';
 
 export class StreamedGraphClient {
     // holds a n3 Store, which is synced to a server-side
@@ -15,7 +10,7 @@
 
     onStatus: (msg: string) => void;
     onGraphChanged: () => void;
-    store: Store;
+    store: N3Store;
     events: EventSource;
     constructor(
         eventsUrl: string,
@@ -28,7 +23,7 @@
         this.onGraphChanged = onGraphChanged;
         this.onStatus('startup...');
 
-        this.store = new Store({});
+        this.store = new Store();
 
         //     //             Object.keys(prefixes).forEach((prefix) => {
         //     //                 this.store.setPrefix(prefix, prefixes[prefix]);
@@ -74,49 +69,36 @@
             }, 3000);
         });
 
-        this.events.addEventListener('fullGraph', (ev) => {
+        this.events.addEventListener('fullGraph', async (ev) => {
             this.onStatus('sync- full graph update');
-            let onReplaced = () => {
-                this.onStatus(`synced ${this.store.size}`);
-                this.onGraphChanged();
-            };
-            this.replaceFullGraph(ev.data, onReplaced);
+            await this.replaceFullGraph(ev.data);
+            this.onStatus(`synced ${this.store.size}`);
+            this.onGraphChanged();
         });
 
-        this.events.addEventListener('patch', (ev) => {
+        this.events.addEventListener('patch', async (ev) => {
             this.onStatus('sync- updating');
-            let onPatched = () => {
-                this.onStatus(`synced ${this.store.size}`);
-                this.onGraphChanged();
-            };
-            this.patchGraph(ev.data, onPatched);
+            await this.patchGraph(ev.data);
+            this.onStatus(`synced ${this.store.size}`);
+            this.onGraphChanged();
         });
         this.onStatus('connecting...');
     }
 
-    replaceFullGraph(jsonLdText: string, done: () => void) {
-        this.store = new Store({});
-        eachJsonLdQuad(JSON.parse(jsonLdText),
-            this.store.addQuad.bind(this.store),
-            done);
+    // these need some locks
+    async replaceFullGraph(jsonLdText: string) {
+        this.store = new Store();
+        await eachJsonLdQuad(JSON.parse(jsonLdText),
+            this.store.addQuad.bind(this.store));
     }
 
-    patchGraph(patchJson: string, done: () => void) {
+    async patchGraph(patchJson: string) {
         var patch = JSON.parse(patchJson).patch;
 
-        async.series([
-            (done) => {
-                eachJsonLdQuad(patch.deletes,
-                    this.store.removeQuad.bind(this.store), done);
-            },
-            (done) => {
-                eachJsonLdQuad(patch.adds,
-                    this.store.addQuad.bind(this.store), done);
-            },
-            /* seriesDone */ (done) => {
-                done();
-            }
-        ], done);
+        await eachJsonLdQuad(patch.deletes,
+            this.store.removeQuad.bind(this.store));
+        await eachJsonLdQuad(patch.adds,
+            this.store.addQuad.bind(this.store));
     }
 
     close() {
@@ -125,7 +107,7 @@
         }
     }
 
-    testEventUrl(eventsUrl: string): Promise<void> {
+    async testEventUrl(eventsUrl: string): Promise<void> {
         return new Promise<void>((resolve, reject) => {
             this.onStatus('testing connection');
             fetch(eventsUrl, {
--- a/src/suffixLabels.ts	Thu Dec 12 22:52:57 2019 -0800
+++ b/src/suffixLabels.ts	Thu Dec 12 22:57:14 2019 -0800
@@ -1,13 +1,13 @@
 import { Term } from 'n3';
 
-type SuffixesNode = { usedBy: null | string, children: Map<string, SuffixesNode> };
-type DisplayNode = { label: string | null, link?: string };
+type SuffixesNode = { usedBy?: string, children: Map<string, SuffixesNode> };
+type DisplayNode = { label?: string, link?: string };
 class SuffixLabels {
     displayNodes: Map<string, DisplayNode>;
     usedSuffixes: SuffixesNode;
     constructor() {
         this.displayNodes = new Map();
-        this.usedSuffixes = { usedBy: null, children: new Map() };
+        this.usedSuffixes = { usedBy: undefined, children: new Map() };
     }
 
     planDisplayForNode(node: Term) {
@@ -22,7 +22,7 @@
 
         const segments = uri.split('/');
         let curs = this.usedSuffixes;
-        let label = null;
+        let label: string | undefined = undefined;
 
         for (let i = segments.length - 1; i >= 0; i--) {
             const seg = segments[i];
@@ -31,15 +31,15 @@
             }
 
             if (!curs.children.has(seg)) {
-                const child = { usedBy: null, children: new Map() };
+                const child: SuffixesNode = { usedBy: undefined, children: new Map() };
                 curs.children.set(seg, child);
 
-                if (label === null) {
+                if (label === undefined) {
                     label = SuffixLabels._tailSegments(uri, segments.length - i);
                     child.usedBy = uri;
                 }
             }
-            curs = curs.children.get(seg);
+            curs = curs.children.get(seg)!;
         }
         this.displayNodes.set(uri, { label: label });
     }
@@ -50,7 +50,7 @@
         // follow, and the clashing uri can be changed to prepend that
         // one child (since we'll see it again if that one wasn't
         // enough).
-        const clashNode: DisplayNode = this.displayNodes.get(curs.usedBy);
+        const clashNode: DisplayNode = this.displayNodes.get(curs.usedBy!)!;
         const nextLeftSeg = curs.children.entries().next().value;
         if (nextLeftSeg[1].usedBy) {
             throw new Error("unexpected");
@@ -58,12 +58,12 @@
 
         clashNode.label = nextLeftSeg[0] + '/' + clashNode.label;
         nextLeftSeg[1].usedBy = curs.usedBy;
-        curs.usedBy = null;
+        curs.usedBy = undefined;
 
     }
 
     getLabelForNode(node: string) {
-        return this.displayNodes.get(node).label;
+        return this.displayNodes.get(node)!.label;
     }
 
     static _tailSegments(uri: string, n: number) {
--- a/tasks.py	Thu Dec 12 22:52:57 2019 -0800
+++ b/tasks.py	Thu Dec 12 22:57:14 2019 -0800
@@ -18,9 +18,12 @@
 @task
 def test(ctx):
     ctx.run(f'node_modules/.bin/webpack-cli --config webpack-test.config.ts')
+<<<<<<< working copy
     ctx.run(f'node_modules/.bin/ts-node node_modules/.bin/jasmine --config=jasmine.json')
 
 # one time:
 # yarn policies set-version v2
 # in vscode, ctrl-p then: ext install ark120202.vscode-typescript-pnp-plugin
 # or see https://next.yarnpkg.com/advanced/pnpify for a compatibility runner.
+=======
+    ctx.run(f'node_modules/.bin/ts-node node_modules/.bin/jasmine --config=jasmine.json')>>>>>>> merge rev
--- a/webpack-dev.config.ts	Thu Dec 12 22:52:57 2019 -0800
+++ b/webpack-dev.config.ts	Thu Dec 12 22:57:14 2019 -0800
@@ -1,35 +1,60 @@
-import path from "path";
-import webpack from 'webpack';
+const path = require("path");
+const webpack = require('webpack');
+
+const resolveConfig = {
+    alias: {
+        'webpack-plugin-serve/client': './node_modules/webpack-plugin-serve/client.js',
+    },
+    extensions: ['.ts', '.js', '.json']
+};
 
-const config: webpack.Configuration = {
+const moduleConfig = {
+    rules: [
+        {
+            test: /\.ts$/,
+            loader: 'ts-loader',
+        },
+        {
+            test: /\.css$/i,
+            use: ['file-loader']
+        },
+        {
+            test: /zzzzz\.js$/, use: {
+                loader: 'babel-loader',
+                options: {
+                }
+            }
+        }
+    ]
+};
+const pluginsConfig = [
+];
+module.exports = {
+    name: "dev",
     mode: "development",
     entry: [
         './src/streamed-graph.ts',
-        './src/streamed-graph.css'   // doesn't emit anything
+        // './src/streamed-graph.css'   // doesn't emit anything
     ],
     output: {
         filename: 'streamed-graph.bundle.js',
         path: path.resolve(__dirname, 'build')
     },
-    resolve: {
-        alias: {
-            'webpack-plugin-serve/client': './node_modules/webpack-plugin-serve/client.js',
-        },
-        extensions: ['.ts', '.js', '.json']
-    },
-    module: {
-        rules: [
-            { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ },
-            { test: /\.css$/i, use: ['file-loader'] },
-        ]
-    },
+    resolve: resolveConfig,
+    devtool: 'source-map',
+    module: moduleConfig,
+    plugins: pluginsConfig,
     devServer: {
         port: 8082,
         hot: false,
         liveReload: true, // doesn't work
         overlay: true,
-        watchContentBase: true,
+        watchContentBase: true
+    },
+    watch: true,
+    watchOptions: {
+        ignored: /node_modules/,
+        poll: 200
     }
 };
 
-export default config;
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webpack-test.config.ts	Thu Dec 12 22:57:14 2019 -0800
@@ -0,0 +1,50 @@
+import path from "path";
+import webpack from 'webpack';
+import { CheckerPlugin } from 'awesome-typescript-loader';
+
+const resolveConfig = {
+    alias: {
+        'webpack-plugin-serve/client': './node_modules/webpack-plugin-serve/client.js',
+    },
+    extensions: ['.ts', '.js', '.json']
+};
+
+const moduleConfig = {
+    rules: [
+        {
+            test: /\.ts$/,
+            use: ['awesome-typescript-loader'],
+            exclude: /node_modules/
+        },
+        {
+            test: /\.css$/i,
+            use: ['file-loader']
+        },
+        {
+            test: /zzzzz\.js$/, use: {
+                loader: 'babel-loader',
+                options: {
+                }
+            }
+        }
+    ]
+};
+const pluginsConfig = [
+    new CheckerPlugin()
+];
+export default {
+    name: "test",
+    mode: "development",
+    entry: [
+        "./src/json_ld_quads_test.ts"
+    ],
+    output: {
+        filename: "test.bundle.js",
+        path: path.resolve(__dirname, 'build')
+    },
+
+    resolve: resolveConfig,
+    module: moduleConfig,
+    plugins: pluginsConfig
+};
+