Changeset - 33f65e2d0e59
[Not reviewed]
default
1 4 0
drewp@bigasterisk.com - 3 years ago 2022-05-24 07:00:38
drewp@bigasterisk.com
who needs a single emitter of all graph change events that anyone on the page can find?
if it's you, revert this change
5 files changed with 12 insertions and 77 deletions:
0 comments (0 inline, 0 general)
light9/collector/web/Light9CollectorUi.ts
Show inline comments
 
import debug from "debug";
 
import { html, LitElement } from "lit";
 
import { customElement, property, state } from "lit/decorators.js";
 
import ReconnectingWebSocket from "reconnectingwebsocket";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { sortBy, uniq } from "underscore";
 
import { Patch } from "../../web/patch";
 
import { getTopGraph } from "../../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "../../web/SyncedGraph";
 
import { GraphAwarePage } from "../../web/GraphAwarePage";
 
import { getTopGraph, GraphChangedEvent } from "../../web/RdfdbSyncedGraph";
 
import { NamedNode } from "n3";
 
import { Patch } from "../../web/patch";
 
import { linkHorizontal } from "d3";
 

	
 
export { Light9CollectorDevice } from "../../web/collector/Light9CollectorDevice";
 
export { RdfdbSyncedGraph } from "../../web/RdfdbSyncedGraph";
 
export { Light9CollectorDevice } from "../../web/collector/Light9CollectorDevice";
 

	
 
debug.enable("*");
 
const log = debug("collector");
 

	
 
@customElement("light9-collector-ui")
 
export class Light9CollectorUi extends GraphAwarePage {
 
export class Light9CollectorUi extends LitElement {
 
  graph!: SyncedGraph;
 
  render() {
 
    return html`${super.render()}
 
    return html`<rdfdb-synced-graph></rdfdb-synced-graph>
 
      <h1>Collector <a href="metrics">[metrics]</a></h1>
 

	
 
      <h2>Devices</h2>
 
      <light9-collector-device-list></light9-collector-device-list> `;
 
  }
 
      <div style="column-width: 11em">${this.devices.map((d) => html`<light9-collector-device uri="${d.value}"></light9-collector-device>`)}</div> `;
 
}
 

	
 
@customElement("light9-collector-device-list")
 
export class Light9CollectorDeviceList extends LitElement {
 
  graph!: SyncedGraph;
 
  @property() devices: NamedNode[] = [];
 
  
 
  render() {
 
    return html`
 
      <h2>Devices</h2>
 
      <div style="column-width: 11em">${this.devices.map((d) => html`<light9-collector-device uri="${d.value}"></light9-collector-device>`)}</div>
 
    `;
 
  }
 
  
 
  constructor() {
 
    super();
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.graph.runHandler(this.findDevices.bind(this), "findDevices");
 
    });
 
  }
 
  
 
  findDevices(patch?: Patch) {
 
    const U = this.graph.U();
 
    
 
    this.devices = [];
 
    let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass"));
 
    uniq(sortBy(classes, "value"), true).forEach((dc) => {
 
      sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => {
 
        this.devices.push(dev as NamedNode);
 
      });
 
    });
 
  }
 
}
light9/web/GraphAwarePage.ts
Show inline comments
 
deleted file
light9/web/RdfdbSyncedGraph.ts
Show inline comments
 
import debug from "debug";
 
import { html, LitElement, css } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { Patch } from "./patch";
 
import { SyncedGraph } from "./SyncedGraph";
 

	
 
const log = debug("syncedgraph-el");
 

	
 
// consider https://vitaly-t.github.io/sub-events/ for this stuff
 
export interface GraphChangedDetail {
 
  graph: SyncedGraph;
 
  patch: Patch;
 
}
 

	
 
export class GraphChangedEvent extends CustomEvent<GraphChangedDetail> {
 
  constructor(type: string, opts: { detail: GraphChangedDetail; bubbles: boolean; composed: boolean }) {
 
    super(type, opts);
 
  }
 
}
 

	
 
let setTopGraph: (sg: SyncedGraph) => void;
 
(window as any).topSyncedGraph = new Promise<SyncedGraph>((res, rej) => {
 
  setTopGraph = res;
 
});
 

	
 
// Contains a SyncedGraph,
 
// displays a little status box,
 
// and emits 'changed' events with the graph and latest patch when it changes
 
@customElement("rdfdb-synced-graph")
 
export class RdfdbSyncedGraph extends LitElement {
 
  @property() graph: SyncedGraph;
 
  @property() status: string;
 
  @property() testGraph = false;
 
  static styles = [
 
    css`
 
      :host {
 
        display: inline-block;
 
        border: 1px solid gray;
 
        min-width: 22em;
 
        background: #05335a;
 
        color: #4fc1d4;
 
      }
 
    `,
 
  ];
 
  render() {
 
    return html`graph: ${this.status}`;
 
  }
 

	
 
  onClear() {
 
    console.log("reset");
 
  }
 

	
 
  constructor() {
 
    super();
 
    this.status = "startup";
 
    this.graph = new SyncedGraph(
 
      this.testGraph ? null : "/rdfdb/api/syncedGraph",
 
      {
 
        "": "http://light9.bigasterisk.com/",
 
        dev: "http://light9.bigasterisk.com/device/",
 
        rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
 
        rdfs: "http://www.w3.org/2000/01/rdf-schema#",
 
        xsd: "http://www.w3.org/2001/XMLSchema#",
 
      },
 
      (s: string) => {
 
        this.status = s;
 
      },
 
      this.onClear.bind(this),
 
      this.onGraphChanged.bind(this)
 
    );
 
    setTopGraph(this.graph);
 
  }
 

	
 
  private onGraphChanged(graph: SyncedGraph, patch: Patch) {
 
    this.dispatchEvent(
 
      new GraphChangedEvent("changed", {
 
        detail: { graph, patch },
 
        bubbles: true,
 
        composed: true,
 
      })
 
    );
 
  }
 
}
 

	
 
export async function getTopGraph(): Promise<SyncedGraph> {
 
  const s = (window as any).topSyncedGraph;
 
  return await s;
 
}
light9/web/SyncedGraph.ts
Show inline comments
 
import * as d3 from "d3";
 
import debug from "debug";
 
import * as N3 from "n3";
 
import { Quad, Quad_Object, Quad_Predicate, Quad_Subject } from "n3";
 
import { sortBy, unique } from "underscore";
 
import { AutoDependencies, HandlerFunc } from "./AutoDependencies";
 
import { Patch, patchSizeSummary } from "./patch";
 
import { RdfDbClient } from "./rdfdbclient";
 
const log = debug("graph");
 

	
 
const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
 

	
 
export class SyncedGraph {
 
  private _autoDeps: AutoDependencies;
 
  private _client: RdfDbClient;
 
  private graph: N3.Store;
 
  cachedFloatValues: any;
 
  cachedUriValues: any;
 
  prefixFuncs: (x: string) => string = (x) => x;
 
  serial: any;
 
  _nextNumber: any;
 
  // Main graph object for a browser to use. Consider using RdfdbSyncedGraph element to create & own
 
  // one of these. Syncs both ways with rdfdb. Meant to hide the choice of RDF lib, so we can change it
 
  // later.
 
  //
 
  // Note that _applyPatch is the only method to write to the graph, so
 
  // it can fire subscriptions.
 

	
 
  constructor(
 
    // url is the /syncedGraph path of an rdfdb server.
 
    public url: any,
 
    // prefixes can be used in Uri(curie) calls.
 
    public prefixes: { [short: string]: string },
 
    private setStatus: any,
 
    // called if we clear the graph
 
    private clearCb: any,
 
    private onGraphChanged: (graph: SyncedGraph, newPatch: Patch)=>void
 
    private clearCb: any
 
  ) {
 
    this.graph = new N3.Store();
 
    this._autoDeps = new AutoDependencies();
 
    this.clearGraph();
 

	
 
    this._client = new RdfDbClient(this.url, this._clearGraphOnNewConnection.bind(this), this._applyPatch.bind(this), this.setStatus);
 
  }
 

	
 
  clearGraph() {
 
    // just deletes the statements; watchers are unaffected.
 
    this.cachedFloatValues = new Map(); // s + '|' + p -> number
 
    this.cachedUriValues = new Map(); // s + '|' + p -> Uri
 

	
 
    this._applyPatch({ adds: [], dels: this.graph.getQuads(null, null, null, null) });
 
    // if we had a Store already, this lets N3.Store free all its indices/etc
 
    this.graph = new N3.Store();
 
    this._addPrefixes(this.prefixes);
 
  }
 

	
 
  _clearGraphOnNewConnection() {
 
    // must not send a patch to the server!
 
    log("clearGraphOnNewConnection");
 
    this.clearGraph();
 
    log("clearGraphOnNewConnection done");
 
    if (this.clearCb != null) {
 
      return this.clearCb();
 
    }
 
  }
 

	
 
  _addPrefixes(prefixes: { [x: string]: string }) {
 
    for (let k of Array.from(prefixes || {})) {
 
      this.prefixes[k] = prefixes[k];
 
    }
 
    this.prefixFuncs = N3.Util.prefixes(this.prefixes);
 
  }
 

	
 
  U() { // just a shorthand
 
    return this.Uri.bind(this);
 
  }
 

	
 
  Uri(curie: string) {
 
    if (curie == null) {
 
      throw new Error("no uri");
 
    }
 
    if (curie.match(/^http/)) {
 
      return N3.DataFactory.namedNode(curie);
 
    }
 
    const part = curie.split(":");
 
@@ -140,97 +139,96 @@ export class SyncedGraph {
 
      throw new Error(`corrupt patch: ${JSON.stringify(patch)}`);
 
    }
 

	
 
    this._validatePatch(patch);
 

	
 
    this._applyPatch(patch);
 
    if (this._client) {
 
      this._client.sendPatch(patch);
 
    }
 
    return console.timeEnd("applyAndSendPatch");
 
  }
 

	
 
  _validatePatch(patch: Patch) {
 
    return [patch.adds, patch.dels].map((qs: Quad[]) =>
 
      (() => {
 
        const result = [];
 
        for (let q of Array.from(qs)) {
 
          if (!q.equals) {
 
            throw new Error("doesn't look like a proper Quad");
 
          }
 
          if (!q.subject.id || q.graph.id == null || q.predicate.id == null) {
 
            throw new Error(`corrupt patch: ${JSON.stringify(q)}`);
 
          } else {
 
            result.push(undefined);
 
          }
 
        }
 
        return result;
 
      })()
 
    );
 
  }
 

	
 
  _applyPatch(patch: Patch) {
 
    // In most cases you want applyAndSendPatch.
 
    //
 
    // This is the only method that writes to this.graph!
 
    log("patch from server [1]")
 
    this.cachedFloatValues.clear();
 
    this.cachedUriValues.clear();
 
    for (let quad of Array.from(patch.dels)) {
 
      //log("remove #{JSON.stringify(quad)}")
 
      const did = this.graph.removeQuad(quad);
 
    }
 
    //log("removed: #{did}")
 
    for (let quad of Array.from(patch.adds)) {
 
      this.graph.addQuad(quad);
 
    }
 
    log("applied patch locally", patchSizeSummary(patch));
 
    this._autoDeps.graphChanged(patch);
 
    this.onGraphChanged(this, patch);
 
  }
 

	
 
  getObjectPatch(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object, g: N3.NamedNode): Patch {
 
    // make a patch which removes existing values for (s,p,*,c) and
 
    // adds (s,p,newObject,c). Values in other graphs are not affected.
 
    const existing = this.graph.getQuads(s, p, null, g);
 
    return {
 
      dels: existing,
 
      adds: [this.Quad(s, p, newObject, g)],
 
    };
 
  }
 

	
 
  patchObject(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object, g: N3.NamedNode) {
 
    this.applyAndSendPatch(this.getObjectPatch(s, p, newObject, g));
 
  }
 

	
 
  clearObjects(s: N3.NamedNode, p: N3.NamedNode, g: N3.NamedNode) {
 
    return this.applyAndSendPatch({
 
      dels: this.graph.getQuads(s, p, null, g),
 
      adds: [],
 
    });
 
  }
 

	
 
  runHandler(func: HandlerFunc, label: string) {
 
    // runs your func once, tracking graph calls. if a future patch
 
    // matches what you queried, we runHandler your func again (and
 
    // forget your queries from the first time).
 

	
 
    // helps with memleak? not sure yet. The point was if two matching
 
    // labels get puushed on, we should run only one. So maybe
 
    // appending a serial number is backwards.
 
    if (!this.serial) {
 
      this.serial = 1;
 
    }
 
    this.serial += 1;
 
    //label = label + @serial
 

	
 
    this._autoDeps.runHandler(func, label);
 
  }
 

	
 
  _singleValue(s: Quad_Subject, p: Quad_Predicate) {
 
    this._autoDeps.askedFor(s, p, null, null);
 
    const quads = this.graph.getQuads(s, p, null, null);
 
    const objs = new Set(Array.from(quads).map((q: Quad) => q.object));
 

	
 
    switch (objs.size) {
 
      case 0:
 
        throw new Error("no value for " + s.value + " " + p.value);
light9/web/collector/Light9CollectorDevice.ts
Show inline comments
 
import debug from "debug";
 
import { css, html, LitElement } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { GraphChangedEvent } from "../RdfdbSyncedGraph";
 
export {ResourceDisplay} from "../ResourceDisplay"
 
export { ResourceDisplay } from "../ResourceDisplay";
 

	
 
debug.enable("*");
 
const log = debug("device-el");
 

	
 
@customElement("light9-collector-device")
 
export class Light9CollectorDevice extends LitElement {
 
  static styles = [
 
    css`
 
      :host {
 
        display: block;
 
        break-inside: avoid-column;
 
        font-size: 80%;
 
      }
 
      h3 {
 
        margin-top: 12px;
 
        margin-bottom: 0;
 
      }
 
      td {
 
        white-space: nowrap;
 
      }
 

	
 
      td.nonzero {
 
        background: #310202;
 
        color: #e25757;
 
      }
 
      td.full {
 
        background: #2b0000;
 
        color: red;
 
        font-weight: bold;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <h3><resource-display .uri=${this.uri}></resource-display></h3>
 
      <table class="borders">
 
        <tr>
 
          <th>out attr</th>
 
          <th>value</th>
 
          <th>chan</th>
 
        </tr>
 
        ${this.attrs.map(
 
          (item) => html`
 
            <tr>
 
              <td>${item.attr}</td>
 
              <td class=${item.valClass}>${item.val} →</td>
 
              <td>${item.chan}</td>
 
            </tr>
 
          `
 
        )}
 
      </table>
 
    `;
 
  }
 
  @property({
 
    // todo don't rebuild uri; pass it right
 
    converter: (s: string | null) => new NamedNode(s || ""),
 
  })
 
  uri: NamedNode = new NamedNode("");
 
  @property() attrs: Array<{ attr: string; valClass: string; val: string; chan: string }> = [];
 

	
 
  constructor() {
 
    super();
 
    // addGraphChangeListener(this.onGraphChanged.bind(this));
 
  }
 
  onChanged(ev: GraphChangedEvent) {
 
    log("patch from server [5]");
 
  }
 
  //  observers: [
 
  //    "initUpdates(updates)",
 
  //  ],
 
  // initUpdates(updates) {
 
  //   updates.addListener(function (msg) {
 
  //     if (msg.outputAttrsSet && msg.outputAttrsSet.dev == this.uri.value) {
 
  //       this.set("attrs", msg.outputAttrsSet.attrs);
 
  //       this.attrs.forEach(function (row) {
 
  //         row.valClass = row.val == 255 ? "full" : row.val ? "nonzero" : "";
 
  //       });
 
  //     }
 
  //   });
 
  // }
 
}
0 comments (0 inline, 0 general)