changeset 139:cf642d395be4

new simpler Patch class; fancier 'hide' view config support
author drewp@bigasterisk.com
date Mon, 08 May 2023 13:05:20 -0700
parents ea0b4e46de2b
children 2ad4784c0d6c
files src/Patch.ts src/SourceGraph.ts src/elements/graph-view/GraphView.ts src/layout/Layout.ts src/layout/ViewConfig.ts
diffstat 5 files changed, 145 insertions(+), 65 deletions(-) [+]
line wrap: on
line diff
--- a/src/Patch.ts	Sat May 06 15:35:11 2023 -0700
+++ b/src/Patch.ts	Mon May 08 13:05:20 2023 -0700
@@ -1,19 +1,24 @@
 import { Quad, Store } from "n3";
 import { Stream } from "rdf-js";
 
+export enum PatchDirection {
+  ADD = "+",
+  DEL = "-",
+}
+
 export class Patch {
-  delQuads: Quad[] = [];
-  addQuads: Quad[] = [];
+  quads: Quad[] = [];
+
+  constructor(public direction: PatchDirection) {}
+
   toString(): string {
-    return `Patch -${this.delQuads.length} +${this.addQuads.length}`;
+    return `Patch ${this.direction} ${this.quads.length}`;
   }
-  constructor() { }
 
-  // fill `addQuads` with this stream
   public async streamImport(quadStream: Stream): Promise<void> {
     return new Promise((resolve, reject) => {
       quadStream.on("data", (quad) => {
-        this.addQuads.push(quad);
+        this.quads.push(quad);
       });
       quadStream.on("error", reject);
       quadStream.on("end", resolve);
@@ -21,7 +26,13 @@
   }
 
   public applyToStore(s: Store) {
-    s.removeQuads(this.delQuads);
-    s.addQuads(this.addQuads);
+    if (this.direction == PatchDirection.ADD) {
+      s.addQuads(this.quads);
+    } else {
+      s.removeQuads(this.quads);
+    }
+  }
+  public isEmpty(): boolean {
+    return this.quads.length == 0;
   }
 }
--- a/src/SourceGraph.ts	Sat May 06 15:35:11 2023 -0700
+++ b/src/SourceGraph.ts	Mon May 08 13:05:20 2023 -0700
@@ -1,7 +1,17 @@
 import { JsonLdParser } from "jsonld-streaming-parser";
 import { Parser, Store } from "n3";
 import { SubEvent } from "sub-events";
-import { Patch } from "./Patch";
+import { Patch, PatchDirection } from "./Patch";
+
+type JsonLdData = Object;
+
+interface CombinedPatchBody {
+  adds?: JsonLdData;
+  deletes?: JsonLdData;
+}
+interface CombinedPatchEvent {
+  patch: CombinedPatchBody;
+}
 
 // Possibly-changing graph for one source. Maintains its own dataset.
 // Read-only.
@@ -22,6 +32,7 @@
   //
   async reloadRdf() {
     const resp = await fetch(this.url);
+    this.clearStore();
     const ctype = resp.headers.get("content-type");
     if (ctype?.startsWith("text/event-stream")) {
       await this.reloadEventStream();
@@ -30,43 +41,85 @@
     }
   }
 
+  private clearStore() {
+    this.store.removeMatches(null, null, null, null);
+  }
+
+  async makePatchFromParsed(
+    dir: PatchDirection,
+    jsonLdObj: Object | undefined
+  ): Promise<Patch> {
+    const p = new Patch(dir);
+    if (jsonLdObj === undefined) {
+      return p;
+    }
+    const parser = new JsonLdParser();
+
+    parser.write(JSON.stringify(jsonLdObj));
+    parser.end();
+
+    await p.streamImport(parser);
+    return p;
+  }
+
+  private applyPatch(p: Patch) {
+    if (p.isEmpty()){
+      return;
+    }
+    p.applyToStore(this.store);
+    if (this.sourceGraphChanged.getStat().unnamed == 0) {
+      console.warn("no listeners to this sourceGraphChanged event");
+    }
+    this.sourceGraphChanged.emit(p);
+  }
+
   private async reloadEventStream(): Promise<void> {
     return new Promise((res, rej) => {
-      //  todo deal with reconnects
       const ev = new EventSource(this.url);
-      let firstPatch = true;
-      // clear store here?
 
-      // maybe the event messages should be 'add' and 'del',
-      // for less parsing and clearer order of ops.
-      ev.addEventListener("patch", async (ev) => {
-        // this is reentrant- ok?
-        
-        const patchMsg = JSON.parse((ev as any).data);
-
-        const p = new Patch();
-
+      // old-style fullGraph. I now think it would be better to send plain
+      // adds and for this SourceGraph to emptyGraph when there's a new connection.
+      ev.addEventListener("fullGraph", async (ev) => {
+        this.clearStore();
+        const p = new Patch(PatchDirection.ADD);
         const parser = new JsonLdParser();
-        parser.write(JSON.stringify(patchMsg.patch.adds));
+        parser.write(ev.data);
         parser.end();
 
         await p.streamImport(parser);
+        p.applyToStore(this.store);
         this.isCurrent = true;
-
-        p.applyToStore(this.store);
-        console.log("patchlife0: eventsream store changed");
-        // firing before there are listeners
         this.sourceGraphChanged.emit(p);
-        if (firstPatch) {
-          firstPatch = false;
-          res();
-        }
+        res();
       });
+
+      // this is for old-style dels-then-adds patches
+      ev.addEventListener("patch", async (ev) => {
+        // this is reentrant- patches might be able to get applied out of order!
+
+        const patchMsg: CombinedPatchEvent = JSON.parse((ev as any).data);
+
+        const p0 = await this.makePatchFromParsed(
+          PatchDirection.DEL,
+          patchMsg.patch.deletes
+        );
+        const p1 = await this.makePatchFromParsed(
+          PatchDirection.ADD,
+          patchMsg.patch.adds
+        );
+
+        this.applyPatch(p0);
+        this.applyPatch(p1);
+
+        this.isCurrent = true;
+      });
+      // here, add listeners for new-style add/del patches
     });
   }
 
   private async reloadSimpleFile(resp: Response) {
     const bodyText = await resp.text();
+    const p = new Patch(PatchDirection.ADD);
     const parser = new Parser({ format: "application/trig" });
     await new Promise<void>((res, rej) => {
       parser.parse(bodyText, (error, quad, prefixes) => {
@@ -76,16 +129,14 @@
           return;
         }
         if (quad) {
-          this.store.addQuad(quad); // prob want to do this as one or a small number of patches
+          p.quads.push(quad);
         } else {
           res();
         }
       });
     });
+    this.applyPatch(p);
     this.isCurrent = true;
-    // this may have to fire per addQuad call for correctness, or else we batch the adds where no readers can see them in advance.
-    console.log("patchlife0: simple file store changed");
-    this.sourceGraphChanged.emit(new Patch());
   }
 
   quadCount(): number {
--- a/src/elements/graph-view/GraphView.ts	Sat May 06 15:35:11 2023 -0700
+++ b/src/elements/graph-view/GraphView.ts	Mon May 08 13:05:20 2023 -0700
@@ -33,12 +33,12 @@
   static styles = [pageStyle, graphViewStyle];
   update(changedProperties: PropertyValues) {
     if (changedProperties.has("graph") && this.graph) {
-      // const viewUri = new NamedNode(this.viewEl?.getAttribute('uri'))
-      const viewUri = new NamedNode(new URL("#view", document.baseURI).href);
+      const viewUri = new NamedNode(this.viewEl?.getAttribute('uri') || 'no-view')
+      // const viewUri = new NamedNode(new URL("#view", document.baseURI).href);
       this.viewConfig = new ViewConfig(this.graph, viewUri);
 
       // "when viewconfig is updated..."
-      setTimeout(()=>this.requestUpdate(), 1000)
+      // setTimeout(()=>this.requestUpdate(), 1000)
 
       this.graph.graphChanged.subscribe(this.onChange?.bind(this));
     }
@@ -67,14 +67,14 @@
     this._addLabelsForAllTerms(graph, labels);
     labels.planDisplayForNode(this.viewConfig.viewRoot); // todo shoudltn be needed
     this.nodeDisplay = new NodeDisplay(labels);
-    let viewTitle = html` (no view)`;
-    viewTitle = html` using view
+    let viewTitle = html``;
+    viewTitle = html`<h2>View 
       <a href="${this.viewConfig.viewRoot.value}"
-        >{this.nodeDisplay.render(this.viewConfig.viewRoot)}</a
+        >${this.nodeDisplay.render(this.viewConfig.viewRoot)}</a
       >`;
     return html`
       <section>
-        <h2>View: ${viewTitle}</h2>
+        ${viewTitle}
         <div>
           <!-- todo: graphs and provenance.
             These statements are all in the
--- a/src/layout/Layout.ts	Sat May 06 15:35:11 2023 -0700
+++ b/src/layout/Layout.ts	Mon May 08 13:05:20 2023 -0700
@@ -220,10 +220,13 @@
   return out;
 }
 
-function freeStatmentsSection(viewConfig: ViewConfig, stmts: Quad[]): FreeStatements {
+function freeStatmentsSection(
+  viewConfig: ViewConfig,
+  stmts: Quad[]
+): FreeStatements {
   const subjs: NamedNode[] = [];
   stmts.forEach((q) => {
-    if (viewConfig.freeStatementsHidePred.has(q.predicate)) {
+    if (viewConfig.hidePredFrees.has(q.predicate)) {
       return;
     }
     subjs.push(q.subject as NamedNode);
@@ -233,7 +236,10 @@
       const preds: NamedNode[] = [];
       let po = Immutable.Map<NamedNode, Term[]>();
       stmts.forEach((q) => {
-        if (q.subject.equals(subj) && !viewConfig.freeStatementsHidePred.has(q.predicate)) {
+        if (
+          q.subject.equals(subj) &&
+          !viewConfig.hidePredFrees.has(q.predicate)
+        ) {
           const p = q.predicate as NamedNode;
           preds.push(p);
           po = addToValues(po, p, q.object as NamedNode);
@@ -260,6 +266,10 @@
   ) {
     graph.forEach(
       (q: Quad) => {
+        if (this.viewConfig.hideGraphEverywhere.has(q.graph)) {
+          return;
+        }
+
         const tables = tablesWantingSubject.get(q.subject as NamedNode);
 
         if (tables && tables.length) {
@@ -301,6 +311,12 @@
     const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
     const ungrouped: Quad[] = [];
     this.groupAllStatements(graph, tablesWantingSubject, ungrouped);
-    return { sections: this.generateSections(this.viewConfig, tableBuilders, ungrouped) };
+    return {
+      sections: this.generateSections(
+        this.viewConfig,
+        tableBuilders,
+        ungrouped
+      ),
+    };
   }
 }
--- a/src/layout/ViewConfig.ts	Sat May 06 15:35:11 2023 -0700
+++ b/src/layout/ViewConfig.ts	Mon May 08 13:05:20 2023 -0700
@@ -1,8 +1,9 @@
 import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x)
 import { DataFactory, NamedNode, Quad_Predicate, Term } from "n3";
 import { MultiStore } from "../MultiStore";
-import { EX } from "./namespaces";
+import { EX, RDF } from "./namespaces";
 import { uriValue } from "./rdf_value";
+import { Quad_Graph } from "rdf-js";
 const Uri = DataFactory.namedNode;
 
 function firstElem<E>(seq: Iterable<E>): E {
@@ -31,17 +32,17 @@
   viewRoot: NamedNode; // this structure...
   graph: MultiStore; // in this graph...
   tables: TableDesc[] = []; // populates all the rest of these fields for use by Layout
-  freeStatementsHidePred: Immutable.Set<Quad_Predicate> = Immutable.Set();
+  hidePredFrees: Immutable.Set<Quad_Predicate> = Immutable.Set();
+  hideGraphEverywhere: Immutable.Set<Quad_Graph> = Immutable.Set();
 
   constructor(graph: MultiStore, viewUri: NamedNode) {
     this.graph = graph;
     this.viewRoot = viewUri;
     // todo
-    const here = "https://bigasterisk.com/lanscape/";
-    if (this.viewRoot.value.startsWith(here)) {
-      this.viewRoot = new NamedNode(this.viewRoot.value.slice(here.length));
-    }
-    // todo: might need to reread if graph changes
+    // const here = "https://bigasterisk.com/lanscape/";
+    // if (this.viewRoot.value.startsWith(here)) {
+    //   this.viewRoot = new NamedNode(this.viewRoot.value.slice(here.length));
+    // }
     this.read();
   }
 
@@ -56,19 +57,20 @@
   }
 
   private readHides() {
-    for (let hideInstruction of this.graph.getObjects(
-      this.viewRoot,
-      EX("freeStatementsHide"),
-      null
-    )) {
-      for (let pred of this.graph.getObjects(
-        hideInstruction,
-        EX("predicate"),
-        null
-      )) {
-        this.freeStatementsHidePred = this.freeStatementsHidePred.add(
-          pred as Quad_Predicate
-        );
+    for (let instr of this.graph.getObjects(this.viewRoot, EX("hide"), null)) {
+      const types = this.graph.getObjects(instr, RDF("type"), null);
+      if (types.length == 1 && types[0].equals(EX("HideEverywhere"))) {
+        for (let g of this.graph.getObjects(instr, EX("graph"), null)) {
+          this.hideGraphEverywhere = this.hideGraphEverywhere.add(
+            g as Quad_Graph
+          );
+        }
+      } else if (types.length == 1 && types[0].equals(EX("HideFreeStatements"))) {
+        for (let pred of this.graph.getObjects(instr, EX("predicate"), null)) {
+          this.hidePredFrees = this.hidePredFrees.add(pred as Quad_Predicate);
+        }
+      } else {
+        throw new Error(":hide instruction must have 1 valid type");
       }
     }
   }