Changeset - 1a96f8647126
[Not reviewed]
default
4 3 7
drewp@bigasterisk.com - 3 years ago 2022-05-24 06:32:37
drewp@bigasterisk.com
big graph & autodep porting to make collector display labels from a syncedgraph
10 files changed with 562 insertions and 477 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 } from "lit/decorators.js";
 
import { customElement, property, state } from "lit/decorators.js";
 
import ReconnectingWebSocket from "reconnectingwebsocket";
 
import { sortBy, uniq } from "underscore";
 
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";
 

	
 
debug.enable('*');
 
export { RdfdbSyncedGraph } from "../../web/RdfdbSyncedGraph";
 
export { Light9CollectorDevice } from "../../web/collector/Light9CollectorDevice";
 

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

	
 
class Updates {
 
  constructor() {
 
    this.listeners = [];
 
  }
 
  addListener(cb) {
 
    this.listeners.push(cb);
 
  }
 
  onMessage(msg) {
 
    this.listeners.forEach(function (lis) {
 
      lis(msg);
 
    });
 
  }
 
}
 

	
 
@customElement("light9-collector-ui")
 
export class Light9CollectorUi extends LitElement {
 
export class Light9CollectorUi extends GraphAwarePage {
 
  graph?: SyncedGraph;
 
  static styles = [];
 
  render() {
 
    return html`
 
      <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
 

	
 
    return html`${super.render()}
 
      <h1>Collector <a href="metrics">[metrics]</a></h1>
 

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

	
 
@customElement("light9-collector-device-list")
 
export class Light9CollectorDeviceList extends LitElement {
 
  graph!: SyncedGraph;
 
  @property() devices: NamedNode[] = [];
 
  
 
  render() {
 
    return html`
 
      <h2>Devices</h2>
 
      <light9-collector-device uri="http://light9.bigasterisk.com/theater/skyline/device/strip1"></light9-collector-device>
 
      <div style="column-width: 11em">${this.devices.map((d) => html`<light9-collector-device uri="${d.value}"></light9-collector-device>`)}</div>
 
    `;
 
  }
 

	
 
  @property() graph: Object = {};
 
  @property() updates: Updates;
 
  @property() devices: Array<string> = [];
 
  //  observers: [
 
  //    'onGraph(graph)',
 
  //  ],
 

	
 
  
 
  constructor() {
 
    super();
 
    this.updates = new Updates();
 
    const ws = new ReconnectingWebSocket(location.href.replace("http", "ws") + "api/updates");
 
    ws.addEventListener("message", (ev: any) => {
 
      log("ws msg", ev);
 
      this.updates.onMessage(ev.data);
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.graph.runHandler(this.findDevices.bind(this), "findDevices");
 
    });
 
  }
 

	
 
  onGraph(graph) {
 
    this.graph.runHandler(this.findDevices.bind(this), "findDevices");
 
  }
 

	
 
  findDevices() {
 
    var U = function (x) {
 
      return this.graph.Uri(x);
 
    };
 
    this.set("devices", []);
 

	
 
  
 
  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.push("devices", dev);
 
        this.devices.push(dev as NamedNode);
 
      });
 
    });
 
  }
 
}
light9/collector/web/index.html
Show inline comments
 
@@ -2,13 +2,13 @@
 
<html>
 
  <head>
 
    <title>collector</title>
 
    <meta charset="utf-8" />
 

	
 
    <link rel="stylesheet" href="./style.css" />
 
    <script type="module" src="../../collector/Light9CollectorUi"></script>
 
    <script type="module" src="../collector/Light9CollectorUi"></script>
 

	
 
    <style>
 
      td {
 
        white-space: nowrap;
 
      }
 
    </style>
light9/web/AutoDependencies.ts
Show inline comments
 
new file 100644
 
import debug from "debug";
 
import { Quad_Graph, Quad_Object, Quad_Predicate, Quad_Subject } from "n3";
 
import { filter } from "underscore";
 
import { allPatchSubjs, Patch } from "./patch";
 

	
 
const log = debug("autodep");
 

	
 
interface QuadPattern {
 
  subject: Quad_Subject | null;
 
  predicate: Quad_Predicate | null;
 
  object: Quad_Object | null;
 
  graph: Quad_Graph | null;
 
}
 

	
 
// use patch as an optional optimization, but you can't count on it
 
export type HandlerFunc = (p?: Patch) => void;
 

	
 
class Handler {
 
  patterns: QuadPattern[];
 
  innerHandlers: Handler[];
 
  // a function and the quad patterns it cared about
 
  constructor(public func: HandlerFunc | null, public label: string) {
 
    this.patterns = []; // s,p,o,g quads that should trigger the next run
 
    this.innerHandlers = []; // Handlers requested while this one was running
 
  }
 
}
 

	
 
export class AutoDependencies {
 
  handlers: Handler;
 
  handlerStack: Handler[];
 
  constructor() {
 
    // tree of all known Handlers (at least those with non-empty
 
    // patterns). Top node is not a handler.
 
    this.handlers = new Handler(null, "root");
 
    this.handlerStack = [this.handlers]; // currently running
 
  }
 

	
 
  runHandler(func: HandlerFunc, label: string) {
 
    // what if we have this func already? duplicate is safe?
 
    if (label == null) {
 
      throw new Error("missing label");
 
    }
 

	
 
    const h = new Handler(func, label);
 
    const tailChildren = this.handlerStack[this.handlerStack.length - 1].innerHandlers;
 
    const matchingLabel = filter(tailChildren, (c: { label: any }) => c.label === label).length;
 
    // ohno, something depends on some handlers getting run twice :(
 
    if (matchingLabel < 2) {
 
      tailChildren.push(h);
 
    }
 
    //console.time("handler #{label}") 
 
    // todo: this may fire 1-2 times before the
 
    // graph is initially loaded, which is a waste. Try deferring it if we
 
    // haven't gotten the graph yet.
 
    return this._rerunHandler(h, undefined);
 
  }
 
  //console.timeEnd("handler #{label}")
 
  //@_logHandlerTree()
 
  _rerunHandler(handler: Handler, patch?: Patch) {
 
    handler.patterns = [];
 
    this.handlerStack.push(handler);
 
    try {
 
      if (handler.func === null) {
 
        throw new Error("tried to rerun root");
 
      }
 
      handler.func(patch);
 
    } catch (e) {
 
      log("error running handler: ", e);
 
    } finally {
 
      // assuming here it didn't get to do all its queries, we could
 
      // add a *,*,*,* handler to call for sure the next time?
 
      // log('done. got: ', handler.patterns)
 
      this.handlerStack.pop();
 
    }
 
  }
 
  // handler might have no watches, in which case we could forget about it
 
  _logHandlerTree() {
 
    log("handler tree:");
 
    var prn = function (h: Handler, depth: number) {
 
      let indent = "";
 
      for (let i = 0; i < depth; i++) {
 
        indent += "  ";
 
      }
 
      log(`${indent} \"${h.label}\" ${h.patterns.length} pats`);
 
      return Array.from(h.innerHandlers).map((c: any) => prn(c, depth + 1));
 
    };
 
    return prn(this.handlers, 0);
 
  }
 

	
 
  _handlerIsAffected(child: Handler, patchSubjs: Set<string>) {
 
    if (patchSubjs === null) {
 
      return true;
 
    }
 
    if (!child.patterns.length) {
 
      return false;
 
    }
 

	
 
    for (let stmt of Array.from(child.patterns)) {
 
      if (stmt.subject === null) {
 
        // wildcard on subject
 
        return true;
 
      }
 
      if (patchSubjs.has(stmt.subject.value)) {
 
        return true;
 
      }
 
    }
 

	
 
    return false;
 
  }
 

	
 
  graphChanged(patch: Patch) {
 
    // SyncedGraph is telling us this patch just got applied to the graph.
 
    const subjs = allPatchSubjs(patch);
 

	
 
    var rerunInners = (cur: Handler) => {
 
      const toRun = cur.innerHandlers.slice();
 
      for (let child of Array.from(toRun)) {
 
        //match = @_handlerIsAffected(child, subjs)
 
        //continue if not match
 
        //log('match', child.label, match)
 
        //child.innerHandlers = [] # let all children get called again
 
        this._rerunHandler(child, patch);
 
        rerunInners(child);
 
      }
 
    };
 
    return rerunInners(this.handlers);
 
  }
 

	
 
  askedFor(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null, g: Quad_Graph | null) {
 
    // SyncedGraph is telling us someone did a query that depended on
 
    // quads in the given pattern.
 
    const current = this.handlerStack[this.handlerStack.length - 1];
 
    if (current != null && current !== this.handlers) {
 
      return current.patterns.push({ subject: s, predicate: p, object: o, graph: g } as QuadPattern);
 
    }
 
  }
 
}
light9/web/GraphAwarePage.ts
Show inline comments
 
new file 100644
 
import debug from "debug";
 
import { html, LitElement } from "lit";
 
import { patchSizeSummary } from "../web/patch";
 
import { GraphChangedEvent } from "../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "./SyncedGraph";
 

	
 
const log = debug("graphaware");
 

	
 
export class GraphAwarePage extends LitElement {
 
  constructor() {
 
    super();
 
    this.classList.add("graph-events");
 
  }
 
  // prepend this to your subclass's render output, like
 
  // render() { return html`${super.render()} ....your page`; }
 
  render() {
 
    return html`<rdfdb-synced-graph @changed="${this.onGraphChanged}"></rdfdb-synced-graph>`;
 
  }
 
  onGraphChanged(ev: GraphChangedEvent) {
 
    log("patch from server [3]", patchSizeSummary(ev.detail.patch));
 
    // this.dispatchEvent(new CustomEvent("changed", { detail: ev.detail }));
 
    log("patch from server [4]");
 
  }
 
}
 

	
light9/web/RdfdbSyncedGraph.ts
Show inline comments
 
file renamed from light9/web/rdfdb-synced-graph.html to light9/web/RdfdbSyncedGraph.ts
 
<link rel="import" href="/lib/polymer/polymer-element.html">
 
<script src="/node_modules/n3/n3-browser.js"></script>
 
<script src="/lib/async/dist/async.js"></script>
 
      <script src="/lib/underscore/underscore-min.js"></script>
 
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 RdfdbSyncedGraph
 

	
 
(window as any).topSyncedGraph = new Promise((res, rej) => {
 
  // Contains a SyncedGraph,
 
  // displays a little status box,
 
  // and emits 'changed' events with the graph and latest patch when it changes
 
  
 
  RdfdbSyncedGraph=customElement("rdfdb-synced-graph")(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}`;
 
    }
 

	
 
<dom-module id="rdfdb-synced-graph">
 
  <template>
 
    <style>
 
     :host {
 
         display: inline-block;
 
         border: 1px solid gray;
 
         min-width: 22em;
 
         background: #05335a;
 
         color: #4fc1d4;
 
     }
 
    </style>
 
    graph: [[status]]
 
  </template>
 
  <script src="rdfdbclient.js"></script>
 
  <script src="graph.js"></script>
 
  <script>
 
   class RdfdbSyncedGraph extends Polymer.Element {
 
       static get is() { return "rdfdb-synced-graph"; }
 
       
 
       static get properties() {
 
           return {
 
               graph: {type: Object, notify: true},
 
               status: {type: String, notify: true},
 
               testGraph: {type: Boolean},
 
           }
 
       }
 
    onClear() {
 
      console.log("reset");
 
    }
 

	
 
       onClear() {
 
           console.log('reset')
 
       }
 
     
 
       connectedCallback() {
 
           super.connectedCallback();
 
           this.graph = new SyncedGraph(
 
               this.testGraph ? null : '/rdfdb/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#',
 
               },
 
             function(s) { this.status = s; }.bind(this),
 
             this.onClear.bind(this));
 
           window.graph = this.graph;
 
       }
 
   }
 
   customElements.define(RdfdbSyncedGraph.is, RdfdbSyncedGraph);
 
  </script>
 
</dom-module>
 
    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)
 
      );
 
      // (window as any).topSyncedGraph = this.graph;
 
      res(this.graph);
 
    }
 

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

	
 

	
 
async function getTopGraph(): Promise<SyncedGraph> {
 
  const s = (window as any).topSyncedGraph;
 
  return await s;
 
}
 

	
 
export { RdfdbSyncedGraph, getTopGraph };
 
\ No newline at end of file
light9/web/ResourceDisplay.ts
Show inline comments
 
file renamed from light9/web/resource-display.html to light9/web/ResourceDisplay.ts
 
<link rel="import" href="/lib/polymer/polymer-element.html">
 
<link rel="import" href="/lib/paper-dialog/paper-dialog.html">
 
<link rel="import" href="/lib/paper-button/paper-button.html">
 
import debug from "debug";
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property, state } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
// import { GraphChangedEvent } from "../../web/RdfdbSyncedGraph";
 
// import { runHandler } from "./GraphAwarePage";
 
import { Patch, patchContainsPreds, patchSizeSummary } from "./patch";
 
import { getTopGraph } from "./RdfdbSyncedGraph";
 
import { SyncedGraph } from "./SyncedGraph";
 
debug.enable("*");
 
const log = debug("device-el");
 
const RDFS_LABEL = new NamedNode("http://www.w3.org/2000/01/rdf-schema#label");
 

	
 
<dom-module id="resource-display">
 
  <template>
 
    <style>
 
     :host {
 
         display: inline-block;
 
     }
 
     
 
     a.resource {
 
         color: inherit;
 
         text-decoration: none;
 
     }
 
@customElement("resource-display")
 
export class ResourceDisplay extends LitElement {
 
  graph!: SyncedGraph;
 
  static styles = [
 
    css`
 
      :host {
 
        display: inline-block;
 
      }
 

	
 
      a.resource {
 
        color: inherit;
 
        text-decoration: none;
 
      }
 

	
 
     .resource {
 
         border: 1px solid #545454;
 
         border-radius: 5px;
 
         padding: 1px;
 
         margin: 2px;
 
         background: rgb(49, 49, 49);
 
         display: inline-block;
 
         text-shadow: 1px 1px 2px black;
 
     }
 
     .resource.minor {
 
         background: none;
 
         border: none;
 
     }
 
     .resource a {
 
         color: rgb(150, 150, 255);
 
         padding: 1px;
 
         display: inline-block;
 
     }
 
     .resource.minor a {
 
         text-decoration: none;
 
         color: rgb(155, 155, 193);
 
         padding: 0;
 
     }
 
    </style>
 
      .resource {
 
        border: 1px solid #545454;
 
        border-radius: 5px;
 
        padding: 1px;
 
        margin: 2px;
 
        background: rgb(49, 49, 49);
 
        display: inline-block;
 
        text-shadow: 1px 1px 2px black;
 
      }
 
      .resource.minor {
 
        background: none;
 
        border: none;
 
      }
 
      .resource a {
 
        color: rgb(150, 150, 255);
 
        padding: 1px;
 
        display: inline-block;
 
      }
 
      .resource.minor a {
 
        text-decoration: none;
 
        color: rgb(155, 155, 193);
 
        padding: 0;
 
      }
 
    `,
 
  ];
 

	
 
    <span class$="[[resClasses]]">
 
      <a href="{{href}}" id="uri">
 
        <!-- type icon goes here -->{{label}}</a>
 
    </span>
 
    <template is="dom-if" if="{{rename}}">
 
      <button on-click="onRename">Rename</button>
 
  render() {
 
    return html` <span class="${this.resClasses()}">
 
      <a href="${this.href()}" id="uri"> <!-- type icon goes here -->${this.label}</a>
 
    </span>`;
 
    // <template is="dom-if" if="{{rename}}">
 
    //   <button on-click="onRename">Rename</button>
 

	
 
    //   <paper-dialog id="renameDialog" modal on-iron-overlay-closed="onRenameClosed">
 
    //     <p>
 
    //       New label:
 
    //       <input id="renameTo" autofocus type="text" value="{{renameTo::input}}" on-keydown="onRenameKey" />
 
    //     </p>
 
    //     <div class="buttons">
 
    //       <paper-button dialog-dismiss>Cancel</paper-button>
 
    //       <paper-button dialog-confirm>OK</paper-button>
 
    //     </div>
 
    //   </paper-dialog>
 
    // </template>
 
    //    `;
 
  }
 
  // callers might set this as string or pass a NamedNode.
 
  @property() uri?: NamedNode | string;
 

	
 
  @state() label: string = "";
 
  @property() rename: boolean = false;
 
  @property() minor: boolean = false;
 
  // @state() renameTo: String; notify: true };
 

	
 
      <paper-dialog id="renameDialog" modal
 
                    on-iron-overlay-closed="onRenameClosed">
 
        <p>
 
          New label:
 
          <input id="renameTo" autofocus type="text"
 
                 value="{{renameTo::input}}"
 
                 on-keydown="onRenameKey">
 
        </p>
 
        <div class="buttons">
 
          <paper-button dialog-dismiss>Cancel</paper-button>
 
          <paper-button dialog-confirm>OK</paper-button>
 
        </div>
 
      </paper-dialog>     
 
    </template>
 
    
 
  </template>
 
  <script>
 
   class ResourceDisplay extends Polymer.Element {
 
     static get is() { return "resource-display"; }
 
     static get properties() {
 
       return {
 
         graph: { type: Object },
 
         // callers might set this as string or NamedNode.
 
         uri: { type: Object }, // Use .value for the string
 
         href: { type: String },
 
         label: { type: String },
 
         rename: { type: Boolean },
 
         minor: { type: Boolean },
 
         resClasses: { type: String, computed: '_resClasses(minor)', value: 'resource' },
 
         renameTo: { type: String, notify: true },
 
       };
 
     }
 
     static get observers() { return ['onUri(graph, uri)']; }
 
     
 
     _resClasses(minor) {
 
       return minor ? 'resource minor' : 'resource';
 
     }
 
     
 
     onUri(graph, uri) {
 
       if (!this.graph) {
 
         this.label = "...";
 
         this.href = "javascript:;'";
 
         return;
 
       }
 
       if (!this.uri) {
 
         this.setLabel();
 
         return;
 
       }
 
       if (typeof uri === 'string') {
 
         uri = this.graph.Uri(uri);
 
       }
 
       this.graph.runHandler(this.setLabel.bind(this),
 
                             `label ${uri.value}`);
 
     }
 
     
 
     setLabel(patch) {
 
       if (!this.uri) {
 
         this.label = "<no uri>";
 
         this.href = "javascript:;";
 
         return;
 
       }
 
       if (patch !== null &&
 
           !SyncedGraph.patchContainsPreds(patch,
 
                                           [this.graph.Uri('rdfs:label')])) {
 
         return;
 
       }
 
       let uri = this.uri;
 
       if (typeof uri === 'string') {
 
         uri = this.graph.Uri(uri);
 
       }
 
       this.label = this.graph.labelOrTail(uri);
 
       this.href = uri.value;
 
     }
 
     
 
     onRename() {
 
       this.renameTo = this.label;
 
       this.shadowRoot.querySelector("#renameDialog").open();
 
       this.shadowRoot.querySelector("#renameTo").setSelectionRange(0, -1);
 
     }
 
     
 
     onRenameKey(ev) {
 
       if (ev.key == 'Enter') {
 
         this.shadowRoot.querySelector("[dialog-confirm]").click();
 
       }
 
       if (ev.key == 'Escape') {
 
         this.shadowRoot.querySelector("[dialog-dismiss]").click();
 
       }
 
     }
 
     
 
     onRenameClosed() {
 
       var dialog = this.shadowRoot.querySelector("#renameDialog");
 
       if (dialog.closingReason.confirmed) {
 
         var label = this.graph.Uri('rdfs:label');
 
         var ctxs = this.graph.contextsWithPattern(this.uri, label, null);
 
         if (ctxs.length != 1) {
 
           throw new Error(
 
             `${ctxs.length} label stmts for ${this.uri.label}`);
 
         }
 
         this.graph.patchObject(
 
           ((typeof this.uri) === 'string' ?
 
            this.graph.Uri(this.uri) : this.uri),
 
           label,
 
           this.graph.Literal(this.renameTo),
 
           ctxs[0]);
 
       }
 
     }
 
   }
 
   customElements.define(ResourceDisplay.is, ResourceDisplay);
 
  </script>
 
</dom-module>
 
  constructor() {
 
    super();
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.runUriHandler();
 
    });
 
  }
 

	
 
  realUri(): NamedNode {
 
    if (!this.uri) {
 
      return new NamedNode("");
 
    }
 
    return typeof this.uri === "string" ? new NamedNode(this.uri) : this.uri;
 
  }
 

	
 
  href() {
 
    if (!this.uri) {
 
      return "javascript:;";
 
    }
 
    return typeof this.uri === "string" ? this.uri : this.uri.value;
 
  }
 

	
 
  updated(changedProperties: PropertyValues) {
 
    if (changedProperties.has("uri")) {
 
      if (!this.graph) {
 
        return; /*too soon*/
 
      }
 
      this.runUriHandler();
 
    }
 
  }
 

	
 
  resClasses() {
 
    return this.minor ? "resource minor" : "resource";
 
  }
 

	
 
  runUriHandler() {
 
    this.graph.runHandler(this.onUri.bind(this), `rdisplay ${this.href()}` /*needs uniqueness?*/);
 
  }
 

	
 
  onUri(patch?: Patch) {
 
    if (!this.uri) {
 
      this.label = "<no uri>";
 
      return;
 
    }
 

	
 
    const uri = this.realUri();
 
    this.graph.runHandler(this.setLabel.bind(this), `label ${uri.value}`);
 
  }
 

	
 
  setLabel(patch?: Patch) {
 
    if (patch && !patchContainsPreds(patch, [RDFS_LABEL])) {
 
      return;
 
    }
 
    const uri = this.realUri();
 
    this.label = this.graph.labelOrTail(uri);
 
  }
 

	
 
  onRename() {
 
    this.renameTo = this.label;
 
    this.shadowRoot.querySelector("#renameDialog").open();
 
    this.shadowRoot.querySelector("#renameTo").setSelectionRange(0, -1);
 
  }
 

	
 
  onRenameKey(ev) {
 
    if (ev.key == "Enter") {
 
      this.shadowRoot.querySelector("[dialog-confirm]").click();
 
    }
 
    if (ev.key == "Escape") {
 
      this.shadowRoot.querySelector("[dialog-dismiss]").click();
 
    }
 
  }
 

	
 
  onRenameClosed() {
 
    var dialog = this.shadowRoot.querySelector("#renameDialog");
 
    if (dialog.closingReason.confirmed) {
 
      var label = this.graph.Uri("rdfs:label");
 
      var ctxs = this.graph.contextsWithPattern(this.uri, label, null);
 
      if (ctxs.length != 1) {
 
        throw new Error(`${ctxs.length} label stmts for ${this.uri.label}`);
 
      }
 
      this.graph.patchObject(typeof this.uri === "string" ? this.graph.Uri(this.uri) : this.uri, label, this.graph.Literal(this.renameTo), ctxs[0]);
 
    }
 
  }
 
}
light9/web/SyncedGraph.ts
Show inline comments
 
file renamed from light9/web/graph.ts to light9/web/SyncedGraph.ts
 
import * as d3 from "d3";
 
import debug from "debug";
 
import * as N3 from "n3";
 
import { Quad, Quad_Subject, Quad_Predicate, Quad_Object, Quad_Graph } from "n3";
 
import { filter, sortBy, unique } from "underscore";
 
import { allPatchSubjs, Patch } from "./patch";
 
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#";
 

	
 
interface QuadPattern {
 
  subject: Quad_Subject | null;
 
  predicate: Quad_Predicate | null;
 
  object: Quad_Object | null;
 
  graph: Quad_Graph | null;
 
}
 

	
 
class Handler {
 
  patterns: QuadPattern[];
 
  innerHandlers: Handler[];
 
  // a function and the quad patterns it cared about
 
  constructor(public func: ((p: Patch) => void) | null, public label: string) {
 
    this.patterns = []; // s,p,o,g quads that should trigger the next run
 
    this.innerHandlers = []; // Handlers requested while this one was running
 
  }
 
}
 

	
 
class AutoDependencies {
 
  handlers: Handler;
 
  handlerStack: Handler[];
 
  constructor() {
 
    // tree of all known Handlers (at least those with non-empty
 
    // patterns). Top node is not a handler.
 
    this.handlers = new Handler(null, "root");
 
    this.handlerStack = [this.handlers]; // currently running
 
  }
 

	
 
  runHandler(func: any, label: any) {
 
    // what if we have this func already? duplicate is safe?
 

	
 
    if (label == null) {
 
      throw new Error("missing label");
 
    }
 

	
 
    const h = new Handler(func, label);
 
    const tailChildren = this.handlerStack[this.handlerStack.length - 1].innerHandlers;
 
    const matchingLabel = filter(tailChildren, (c: { label: any }) => c.label === label).length;
 
    // ohno, something depends on some handlers getting run twice :(
 
    if (matchingLabel < 2) {
 
      tailChildren.push(h);
 
    }
 
    //console.time("handler #{label}")
 
    return this._rerunHandler(h, null);
 
  }
 
  //console.timeEnd("handler #{label}")
 
  //@_logHandlerTree()
 

	
 
  _rerunHandler(handler: Handler, patch: any) {
 
    handler.patterns = [];
 
    this.handlerStack.push(handler);
 
    try {
 
      if (handler.func === null) {
 
        throw new Error("tried to rerun root");
 
      }
 
      return handler.func(patch);
 
    } catch (e) {
 
      return log("error running handler: ", e);
 
    } finally {
 
      // assuming here it didn't get to do all its queries, we could
 
      // add a *,*,*,* handler to call for sure the next time?
 
      //log('done. got: ', handler.patterns)
 
      this.handlerStack.pop();
 
    }
 
  }
 
  // handler might have no watches, in which case we could forget about it
 

	
 
  _logHandlerTree() {
 
    log("handler tree:");
 
    var prn = function (h: Handler, depth: number) {
 
      let indent = "";
 
      for (let i = 0, end = depth, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
 
        indent += "  ";
 
      }
 
      log(`${indent} \"${h.label}\" ${h.patterns.length} pats`);
 
      return Array.from(h.innerHandlers).map((c: any) => prn(c, depth + 1));
 
    };
 
    return prn(this.handlers, 0);
 
  }
 

	
 
  _handlerIsAffected(child: Handler, patchSubjs: Set<string>) {
 
    if (patchSubjs === null) {
 
      return true;
 
    }
 
    if (!child.patterns.length) {
 
      return false;
 
    }
 

	
 
    for (let stmt of Array.from(child.patterns)) {
 
      if (stmt.subject === null) {
 
        // wildcard on subject
 
        return true;
 
      }
 
      if (patchSubjs.has(stmt.subject.value)) {
 
        return true;
 
      }
 
    }
 

	
 
    return false;
 
  }
 

	
 
  graphChanged(patch: Patch) {
 
    // SyncedGraph is telling us this patch just got applied to the graph.
 

	
 
    const subjs = allPatchSubjs(patch);
 

	
 
    var rerunInners = (cur: Handler) => {
 
      const toRun = cur.innerHandlers.slice();
 
      for (let child of Array.from(toRun)) {
 
        //match = @_handlerIsAffected(child, subjs)
 
        //continue if not match
 
        //log('match', child.label, match)
 
        //child.innerHandlers = [] # let all children get called again
 

	
 
        this._rerunHandler(child, patch);
 
        rerunInners(child);
 
      }
 
    };
 
    return rerunInners(this.handlers);
 
  }
 

	
 
  askedFor(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null, g: Quad_Graph | null) {
 
    // SyncedGraph is telling us someone did a query that depended on
 
    // quads in the given pattern.
 
    const current = this.handlerStack[this.handlerStack.length - 1];
 
    if (current != null && current !== this.handlers) {
 
      return current.patterns.push({ subject: s, predicate: p, object: o, graph: g } as QuadPattern);
 
    }
 
  }
 
}
 

	
 
export class SyncedGraph {
 
  _autoDeps: AutoDependencies;
 
  _client: any;
 
  graph: N3.Store;
 
  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. Syncs both ways with
 
  // rdfdb. Meant to hide the choice of RDF lib, so we can change it
 
  // 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(
 
    // patchSenderUrl is the /syncedGraph path of an rdfdb server.
 
    public patchSenderUrl: any,
 
    // 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 clearCb: any,
 
    private onGraphChanged: (graph: SyncedGraph, newPatch: Patch)=>void
 
  ) {
 
    this.graph = new N3.Store();
 
    this._autoDeps = new AutoDependencies(); // replaces GraphWatchers
 
    this._autoDeps = new AutoDependencies();
 
    this.clearGraph();
 

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

	
 
  clearGraph() {
 
    // just deletes the statements; watchers are unaffected.
 
    if (this.graph != null) {
 
      this._applyPatch({ adds: [], dels: this.graph.getQuads(null, null, null, null) });
 
    }
 
    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);
 
    this.cachedFloatValues = new Map(); // s + '|' + p -> number
 
    return (this.cachedUriValues = new Map()); // s + '|' + p -> Uri
 
  }
 

	
 
  _clearGraphOnNewConnection() {
 
    // must not send a patch to the server!
 
    log("graph: clearGraphOnNewConnection");
 
    log("clearGraphOnNewConnection");
 
    this.clearGraph();
 
    log("graph: clearGraphOnNewConnection done");
 
    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);
 
@@ -296,26 +168,27 @@ export class SyncedGraph {
 
    );
 
  }
 

	
 
  _applyPatch(patch: Patch) {
 
    // In most cases you want applyAndSendPatch.
 
    //
 
    // This is the only method that writes to @graph!
 
    let quad: any;
 
    // This is the only method that writes to this.graph!
 
    log("patch from server [1]")
 
    this.cachedFloatValues.clear();
 
    this.cachedUriValues.clear();
 
    for (quad of Array.from(patch.dels)) {
 
    for (let quad of Array.from(patch.dels)) {
 
      //log("remove #{JSON.stringify(quad)}")
 
      const did = this.graph.removeQuad(quad);
 
    }
 
    //log("removed: #{did}")
 
    for (quad of Array.from(patch.adds)) {
 
    for (let quad of Array.from(patch.adds)) {
 
      this.graph.addQuad(quad);
 
    }
 
    //log('applied patch locally', patchSizeSummary(patch))
 
    return this._autoDeps.graphChanged(patch);
 
    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);
 
@@ -323,23 +196,23 @@ export class SyncedGraph {
 
      dels: existing,
 
      adds: [this.Quad(s, p, newObject, g)],
 
    };
 
  }
 

	
 
  patchObject(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object, g: N3.NamedNode) {
 
    return this.applyAndSendPatch(this.getObjectPatch(s, p, newObject, g));
 
    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: any, label: any) {
 
  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
 
@@ -347,13 +220,13 @@ export class SyncedGraph {
 
    if (!this.serial) {
 
      this.serial = 1;
 
    }
 
    this.serial += 1;
 
    //label = label + @serial
 

	
 
    return this._autoDeps.runHandler(func, label);
 
    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));
 
@@ -413,19 +286,19 @@ export class SyncedGraph {
 
    if (!ret) {
 
      ret = uri.value;
 
    }
 
    return ret;
 
  }
 

	
 
  objects(s: any, p: any) {
 
  objects(s: any, p: any): Quad_Object[] {
 
    this._autoDeps.askedFor(s, p, null, null);
 
    const quads = this.graph.getQuads(s, p, null, null);
 
    return Array.from(quads).map((q: { object: any }) => q.object);
 
  }
 

	
 
  subjects(p: any, o: any) {
 
  subjects(p: any, o: any): Quad_Subject[] {
 
    this._autoDeps.askedFor(null, p, o, null);
 
    const quads = this.graph.getQuads(null, p, o, null);
 
    return Array.from(quads).map((q: { subject: any }) => q.subject);
 
  }
 

	
 
  items(list: any) {
 
@@ -451,13 +324,13 @@ export class SyncedGraph {
 
      current = rests[0].object;
 
    }
 

	
 
    return out;
 
  }
 

	
 
  contains(s: any, p: any, o: any) {
 
  contains(s: any, p: any, o: any): boolean {
 
    this._autoDeps.askedFor(s, p, o, null);
 
    log("contains calling getQuads when graph has ", this.graph.size);
 
    return this.graph.getQuads(s, p, o, null).length > 0;
 
  }
 

	
 
  nextNumberedResources(base: { id: any }, howMany: number) {
light9/web/collector/Light9CollectorDevice.ts
Show inline comments
 
file renamed from light9/collector/web/Light9CollectorDevice.ts to light9/web/collector/Light9CollectorDevice.ts
 
import * as debug from "debug";
 
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"
 
debug.enable("*");
 
const log = debug("device-el");
 

	
 
@customElement("light9-collector-device")
 
export class Light9CollectorDevice extends LitElement {
 
  static styles = [
 
    css`
 
      :host {
 
@@ -31,41 +35,53 @@ export class Light9CollectorDevice exten
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <h3><resource-display graph="{{graph}}" uri="{{uri}}"></resource-display></h3>
 
      <h3><resource-display .uri=${this.uri}></resource-display></h3>
 
      <table class="borders">
 
        <tr>
 
          <th>out attr</th>
 
          <th>value</th>
 
          <th>chan</th>
 
        </tr>
 
        <template is="dom-repeat" items="{{attrs}}">
 
          <tr>
 
            <td>{{item.attr}}</td>
 
            <td class$="{{item.valClass}}">{{item.val}} →</td>
 
            <td>{{item.chan}}</td>
 
          </tr>
 
        </template>
 
        ${this.attrs.map(
 
          (item) => html`
 
            <tr>
 
              <td>${item.attr}</td>
 
              <td class=${item.valClass}>${item.val} →</td>
 
              <td>${item.chan}</td>
 
            </tr>
 
          `
 
        )}
 
      </table>
 
    `;
 
  }
 
  @property() graph: Object = {};
 
  @property() uri: Object = {};
 
  @property() attrs: Array = [];
 
  @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" : "";
 
        });
 
      }
 
    });
 
  }
 
  // 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" : "";
 
  //       });
 
  //     }
 
  //   });
 
  // }
 
}
light9/web/collector/README.md
Show inline comments
 
new file 100644
 
this is meant to be at light9/collector/web but I couldn't figure out the vite paths
 
\ No newline at end of file
light9/web/rdfdbclient.ts
Show inline comments
 
@@ -6,16 +6,16 @@ const log = debug("rdfdbclient");
 
export class RdfDbClient {
 
  _patchesToSend: Patch[];
 
  _lastPingMs: number;
 
  _patchesReceived: number;
 
  _patchesSent: number;
 
  _connectionId: string;
 
  _reconnectionTimeout: number | null;
 
  ws: WebSocket | undefined;
 
  _pingLoopTimeout: any;
 
  // Send and receive patches from rdfdb
 
  _reconnectionTimeout?: number;
 
  ws?: WebSocket;
 
  _pingLoopTimeout?: number;
 
  // Send and receive patches from rdfdb. Primarily used in SyncedGraph.
 
  //
 
  // What this should do, and does not yet, is keep the graph
 
  // 'coasting' over a reconnect, applying only the diffs from the old
 
  // contents to the new ones once they're in. Then, remove all the
 
  // clearGraph stuff in graph.coffee that doesn't even work right.
 
  //
 
@@ -27,13 +27,12 @@ export class RdfDbClient {
 
  ) {
 
    this._patchesToSend = [];
 
    this._lastPingMs = -1;
 
    this._patchesReceived = 0;
 
    this._patchesSent = 0;
 
    this._connectionId = "??";
 
    this._reconnectionTimeout = null;
 
    this.ws = undefined;
 

	
 
    this._newConnection();
 
  }
 

	
 
  _updateStatus() {
 
@@ -60,65 +59,67 @@ export class RdfDbClient {
 
${this._patchesSent} sent; \
 
${this._patchesToSend.length} pending; \
 
${ping}ms`);
 
  }
 

	
 
  sendPatch(patch: Patch) {
 
    log("rdfdbclient: queue patch to server ", patchSizeSummary(patch));
 
    log("queue patch to server ", patchSizeSummary(patch));
 
    this._patchesToSend.push(patch);
 
    this._updateStatus();
 
    this._continueSending();
 
  }
 

	
 
  _newConnection() {
 
    const wsOrWss = window.location.protocol.replace("http", "ws");
 
    const fullUrl = wsOrWss + "//" + window.location.host + this.patchSenderUrl;
 
    if (this.ws !== undefined) {
 
      this.ws.close();
 
    }
 
    this.ws = new WebSocket(fullUrl);
 
    this.ws.onopen = this.onWsOpen.bind(this);
 
    this.ws.onerror = this.onWsError.bind(this);
 
    this.ws.onclose = this.onWsClose.bind(this);
 
    this.ws.onmessage = this._onMessage.bind(this);
 
  }
 

	
 
    this.ws.onopen = () => {
 
      log("rdfdbclient: new connection to", fullUrl);
 
      this._updateStatus();
 
      this.clearGraphOnNewConnection();
 
      return this._pingLoop();
 
    };
 
  private onWsOpen() {
 
    log("new connection to", this.patchSenderUrl);
 
    this._updateStatus();
 
    this.clearGraphOnNewConnection();
 
    return this._pingLoop();
 
  }
 

	
 
    this.ws.onerror = (e: Event) => {
 
      log("rdfdbclient: ws error " + e);
 
      if (this.ws !== undefined) {
 
        const closeHandler = this.ws.onclose?.bind(this.ws);
 
        if (!closeHandler) {
 
          throw new Error();
 
        }
 
        closeHandler(new CloseEvent("forced"));
 
  private onWsError(e: Event) {
 
    log("ws error", e);
 
    if (this.ws !== undefined) {
 
      const closeHandler = this.ws.onclose?.bind(this.ws);
 
      if (!closeHandler) {
 
        throw new Error();
 
      }
 
    };
 
      closeHandler(new CloseEvent("forced"));
 
    }
 
  }
 

	
 
    this.ws.onclose = (ev: CloseEvent) => {
 
      log("rdfdbclient: ws close");
 
      this._updateStatus();
 
      if (this._reconnectionTimeout != null) {
 
        clearTimeout(this._reconnectionTimeout);
 
      }
 
      this._reconnectionTimeout = (setTimeout(this._newConnection.bind(this), 1000) as unknown) as number;
 
    };
 

	
 
    this.ws.onmessage = this._onMessage.bind(this);
 
  private onWsClose(ev: CloseEvent) {
 
    log("ws close");
 
    this._updateStatus();
 
    if (this._reconnectionTimeout !== undefined) {
 
      clearTimeout(this._reconnectionTimeout);
 
    }
 
    this._reconnectionTimeout = (setTimeout(this._newConnection.bind(this), 1000) as unknown) as number;
 
  }
 

	
 
  _pingLoop() {
 
    if (this.ws && this.ws.readyState === this.ws.OPEN) {
 
      this.ws.send("PING");
 
      this._lastPingMs = -Date.now();
 

	
 
      if (this._pingLoopTimeout != null) {
 
        clearTimeout(this._pingLoopTimeout);
 
      }
 
      this._pingLoopTimeout = setTimeout(this._pingLoop.bind(this), 10000);
 
      this._pingLoopTimeout = (setTimeout(this._pingLoop.bind(this), 10000) as unknown) as number;
 
    }
 
  }
 

	
 
  _onMessage(evt: { data: string }) {
 
    const msg = evt.data;
 
    if (msg === "PONG") {
 
@@ -128,12 +129,13 @@ export class RdfDbClient {
 
    }
 

	
 
    const input = JSON.parse(msg);
 
    if (input.connectedAs) {
 
      this._connectionId = input.connectedAs;
 
    } else {
 
      log("patch from server [0]")
 
      parseJsonPatch(input, this.applyPatch.bind(this));
 
      this._patchesReceived++;
 
    }
 
    return this._updateStatus();
 
  }
 

	
 
@@ -145,13 +147,13 @@ export class RdfDbClient {
 

	
 
    // we could call this less often and coalesce patches together to optimize
 
    // the dragging cases.
 

	
 
    const sendOne = (patch: any, cb: (arg0: any) => any) => {
 
      return toJsonPatch(patch, (json: string) => {
 
        log("rdfdbclient: send patch to server, " + json.length + " bytes");
 
        log("send patch to server, " + json.length + " bytes");
 
        if (!this.ws) {
 
          throw new Error("can't send");
 
        }
 
        this.ws.send(json);
 
        this._patchesSent++;
 
        this._updateStatus();
0 comments (0 inline, 0 general)