Mercurial > code > home > repos > light9
changeset 2074:1a96f8647126
big graph & autodep porting to make collector display labels from a syncedgraph
author | drewp@bigasterisk.com |
---|---|
date | Mon, 23 May 2022 23:32:37 -0700 |
parents | 17b268d2b7f3 |
children | fddd07e694ab |
files | light9/collector/web/Light9CollectorDevice.ts light9/collector/web/Light9CollectorUi.ts light9/collector/web/index.html light9/web/AutoDependencies.ts light9/web/GraphAwarePage.ts light9/web/RdfdbSyncedGraph.ts light9/web/ResourceDisplay.ts light9/web/SyncedGraph.ts light9/web/collector/Light9CollectorDevice.ts light9/web/collector/README.md light9/web/graph.ts light9/web/rdfdb-synced-graph.html light9/web/rdfdbclient.ts light9/web/resource-display.html |
diffstat | 14 files changed, 984 insertions(+), 899 deletions(-) [+] |
line wrap: on
line diff
--- a/light9/collector/web/Light9CollectorDevice.ts Sun May 22 03:04:18 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -import * as debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -debug.enable("*"); - -@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 graph="{{graph}}" uri="{{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> - </table> - `; - } - @property() graph: Object = {}; - @property() uri: Object = {}; - @property() attrs: Array = []; - - // 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" : ""; - }); - } - }); - } -}
--- a/light9/collector/web/Light9CollectorUi.ts Sun May 22 03:04:18 2022 -0700 +++ b/light9/collector/web/Light9CollectorUi.ts Mon May 23 23:32:37 2022 -0700 @@ -1,75 +1,63 @@ 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); }); }); }
--- a/light9/collector/web/index.html Sun May 22 03:04:18 2022 -0700 +++ b/light9/collector/web/index.html Mon May 23 23:32:37 2022 -0700 @@ -5,7 +5,7 @@ <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 {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/AutoDependencies.ts Mon May 23 23:32:37 2022 -0700 @@ -0,0 +1,137 @@ +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); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/GraphAwarePage.ts Mon May 23 23:32:37 2022 -0700 @@ -0,0 +1,25 @@ +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]"); + } +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/RdfdbSyncedGraph.ts Mon May 23 23:32:37 2022 -0700 @@ -0,0 +1,91 @@ +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}`; + } + + 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) + ); + // (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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/ResourceDisplay.ts Mon May 23 23:32:37 2022 -0700 @@ -0,0 +1,165 @@ +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"); + +@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; + } + `, + ]; + + 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 }; + + 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]); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/SyncedGraph.ts Mon May 23 23:32:37 2022 -0700 @@ -0,0 +1,404 @@ +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 + ) { + 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(":"); + return this.prefixFuncs(part[0])(part[1]); + } + + Literal(jsValue: any) { + return N3.DataFactory.literal(jsValue); + } + + LiteralRoundedFloat(f: number) { + return N3.DataFactory.literal(d3.format(".3f")(f), this.Uri("http://www.w3.org/2001/XMLSchema#double")); + } + + Quad(s: any, p: any, o: any, g: any) { + return N3.DataFactory.quad(s, p, o, g); + } + + toJs(literal: { value: any }) { + // incomplete + return parseFloat(literal.value); + } + + loadTrig(trig: any, cb: () => any) { + // for debugging + const patch: Patch = { dels: [], adds: [] }; + const parser = new N3.Parser(); + return parser.parse(trig, (error: any, quad: any, prefixes: any) => { + if (error) { + throw new Error(error); + } + if (quad) { + return patch.adds.push(quad); + } else { + this._applyPatch(patch); + this._addPrefixes(prefixes); + if (cb) { + return cb(); + } + } + }); + } + + quads(): any { + // for debugging + return Array.from(this.graph.getQuads(null, null, null, null)).map((q: Quad) => [q.subject, q.predicate, q.object, q.graph]); + } + + applyAndSendPatch(patch: Patch) { + console.time("applyAndSendPatch"); + if (!this._client) { + log("not connected-- dropping patch"); + return; + } + if (!Array.isArray(patch.adds) || !Array.isArray(patch.dels)) { + console.timeEnd("applyAndSendPatch"); + log("corrupt patch"); + 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); + case 1: + var obj = objs.values().next().value; + return obj; + default: + throw new Error("too many different values: " + JSON.stringify(quads)); + } + } + + floatValue(s: Quad_Subject, p: Quad_Predicate) { + const key = s.value + "|" + p.value; + const hit = this.cachedFloatValues.get(key); + if (hit !== undefined) { + return hit; + } + //log('float miss', s, p) + + const v = this._singleValue(s, p).value; + const ret = parseFloat(v); + if (isNaN(ret)) { + throw new Error(`${s.value} ${p.value} -> ${v} not a float`); + } + this.cachedFloatValues.set(key, ret); + return ret; + } + + stringValue(s: any, p: any) { + return this._singleValue(s, p).value; + } + + uriValue(s: Quad_Subject, p: Quad_Predicate) { + const key = s.value + "|" + p.value; + const hit = this.cachedUriValues.get(key); + if (hit !== undefined) { + return hit; + } + + const ret = this._singleValue(s, p); + this.cachedUriValues.set(key, ret); + return ret; + } + + labelOrTail(uri: { value: { split: (arg0: string) => any } }) { + let ret: any; + try { + ret = this.stringValue(uri, this.Uri("rdfs:label")); + } catch (error) { + const words = uri.value.split("/"); + ret = words[words.length - 1]; + } + if (!ret) { + ret = uri.value; + } + return ret; + } + + 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): 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) { + const out = []; + let current = list; + while (true) { + if (current === RDF + "nil") { + break; + } + + this._autoDeps.askedFor(current, null, null, null); // a little loose + + const firsts = this.graph.getQuads(current, RDF + "first", null, null); + const rests = this.graph.getQuads(current, RDF + "rest", null, null); + if (firsts.length !== 1) { + throw new Error(`list node ${current} has ${firsts.length} rdf:first edges`); + } + out.push(firsts[0].object); + + if (rests.length !== 1) { + throw new Error(`list node ${current} has ${rests.length} rdf:rest edges`); + } + current = rests[0].object; + } + + return out; + } + + 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) { + // base is NamedNode or string + // Note this is unsafe before we're synced with the graph. It'll + // always return 'name0'. + if (base.id) { + base = base.id; + } + const results = []; + + // @contains is really slow. + if (this._nextNumber == null) { + this._nextNumber = new Map(); + } + let start = this._nextNumber.get(base); + if (start === undefined) { + start = 0; + } + + for (let serial = start, asc = start <= 1000; asc ? serial <= 1000 : serial >= 1000; asc ? serial++ : serial--) { + const uri = this.Uri(`${base}${serial}`); + if (!this.contains(uri, null, null)) { + results.push(uri); + log("nextNumberedResources", `picked ${uri}`); + this._nextNumber.set(base, serial + 1); + if (results.length >= howMany) { + return results; + } + } + } + throw new Error(`can't make sequential uri with base ${base}`); + } + + nextNumberedResource(base: any) { + return this.nextNumberedResources(base, 1)[0]; + } + + contextsWithPattern(s: any, p: any, o: any) { + this._autoDeps.askedFor(s, p, o, null); + const ctxs = []; + for (let q of Array.from(this.graph.getQuads(s, p, o, null))) { + ctxs.push(q.graph); + } + return unique(ctxs); + } + + sortKey(uri: N3.NamedNode) { + const parts = uri.value.split(/([0-9]+)/); + const expanded = parts.map(function (p: string) { + const f = parseInt(p); + if (isNaN(f)) { + return p; + } + return p.padStart(8, "0"); + }); + return expanded.join(""); + } + + sortedUris(uris: any) { + return sortBy(uris, this.sortKey); + } + + prettyLiteral(x: any) { + if (typeof x === "number") { + return this.LiteralRoundedFloat(x); + } else { + return this.Literal(x); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/collector/Light9CollectorDevice.ts Mon May 23 23:32:37 2022 -0700 @@ -0,0 +1,87 @@ +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 { + 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" : ""; + // }); + // } + // }); + // } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/collector/README.md Mon May 23 23:32:37 2022 -0700 @@ -0,0 +1,1 @@ +this is meant to be at light9/collector/web but I couldn't figure out the vite paths \ No newline at end of file
--- a/light9/web/graph.ts Sun May 22 03:04:18 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,531 +0,0 @@ -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 { 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; - 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 - // 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, - // 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 - ) { - this.graph = new N3.Store(); - this._autoDeps = new AutoDependencies(); // replaces GraphWatchers - this.clearGraph(); - - if (this.patchSenderUrl) { - this._client = new RdfDbClient(this.patchSenderUrl, 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) }); - } - - // 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"); - this.clearGraph(); - log("graph: 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); - } - - Uri(curie: string) { - if (curie == null) { - throw new Error("no uri"); - } - if (curie.match(/^http/)) { - return N3.DataFactory.namedNode(curie); - } - const part = curie.split(":"); - return this.prefixFuncs(part[0])(part[1]); - } - - Literal(jsValue: any) { - return N3.DataFactory.literal(jsValue); - } - - LiteralRoundedFloat(f: number) { - return N3.DataFactory.literal(d3.format(".3f")(f), this.Uri("http://www.w3.org/2001/XMLSchema#double")); - } - - Quad(s: any, p: any, o: any, g: any) { - return N3.DataFactory.quad(s, p, o, g); - } - - toJs(literal: { value: any }) { - // incomplete - return parseFloat(literal.value); - } - - loadTrig(trig: any, cb: () => any) { - // for debugging - const patch: Patch = { dels: [], adds: [] }; - const parser = new N3.Parser(); - return parser.parse(trig, (error: any, quad: any, prefixes: any) => { - if (error) { - throw new Error(error); - } - if (quad) { - return patch.adds.push(quad); - } else { - this._applyPatch(patch); - this._addPrefixes(prefixes); - if (cb) { - return cb(); - } - } - }); - } - - quads(): any { - // for debugging - return Array.from(this.graph.getQuads(null, null, null, null)).map((q: Quad) => [q.subject, q.predicate, q.object, q.graph]); - } - - applyAndSendPatch(patch: Patch) { - console.time("applyAndSendPatch"); - if (!this._client) { - log("not connected-- dropping patch"); - return; - } - if (!Array.isArray(patch.adds) || !Array.isArray(patch.dels)) { - console.timeEnd("applyAndSendPatch"); - log("corrupt patch"); - 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 @graph! - let quad: any; - this.cachedFloatValues.clear(); - this.cachedUriValues.clear(); - for (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)) { - this.graph.addQuad(quad); - } - //log('applied patch locally', patchSizeSummary(patch)) - return this._autoDeps.graphChanged(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) { - return 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) { - // 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 - - return 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); - case 1: - var obj = objs.values().next().value; - return obj; - default: - throw new Error("too many different values: " + JSON.stringify(quads)); - } - } - - floatValue(s: Quad_Subject, p: Quad_Predicate) { - const key = s.value + "|" + p.value; - const hit = this.cachedFloatValues.get(key); - if (hit !== undefined) { - return hit; - } - //log('float miss', s, p) - - const v = this._singleValue(s, p).value; - const ret = parseFloat(v); - if (isNaN(ret)) { - throw new Error(`${s.value} ${p.value} -> ${v} not a float`); - } - this.cachedFloatValues.set(key, ret); - return ret; - } - - stringValue(s: any, p: any) { - return this._singleValue(s, p).value; - } - - uriValue(s: Quad_Subject, p: Quad_Predicate) { - const key = s.value + "|" + p.value; - const hit = this.cachedUriValues.get(key); - if (hit !== undefined) { - return hit; - } - - const ret = this._singleValue(s, p); - this.cachedUriValues.set(key, ret); - return ret; - } - - labelOrTail(uri: { value: { split: (arg0: string) => any } }) { - let ret: any; - try { - ret = this.stringValue(uri, this.Uri("rdfs:label")); - } catch (error) { - const words = uri.value.split("/"); - ret = words[words.length - 1]; - } - if (!ret) { - ret = uri.value; - } - return ret; - } - - objects(s: any, p: any) { - 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) { - 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) { - const out = []; - let current = list; - while (true) { - if (current === RDF + "nil") { - break; - } - - this._autoDeps.askedFor(current, null, null, null); // a little loose - - const firsts = this.graph.getQuads(current, RDF + "first", null, null); - const rests = this.graph.getQuads(current, RDF + "rest", null, null); - if (firsts.length !== 1) { - throw new Error(`list node ${current} has ${firsts.length} rdf:first edges`); - } - out.push(firsts[0].object); - - if (rests.length !== 1) { - throw new Error(`list node ${current} has ${rests.length} rdf:rest edges`); - } - current = rests[0].object; - } - - return out; - } - - contains(s: any, p: any, o: any) { - 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) { - // base is NamedNode or string - // Note this is unsafe before we're synced with the graph. It'll - // always return 'name0'. - if (base.id) { - base = base.id; - } - const results = []; - - // @contains is really slow. - if (this._nextNumber == null) { - this._nextNumber = new Map(); - } - let start = this._nextNumber.get(base); - if (start === undefined) { - start = 0; - } - - for (let serial = start, asc = start <= 1000; asc ? serial <= 1000 : serial >= 1000; asc ? serial++ : serial--) { - const uri = this.Uri(`${base}${serial}`); - if (!this.contains(uri, null, null)) { - results.push(uri); - log("nextNumberedResources", `picked ${uri}`); - this._nextNumber.set(base, serial + 1); - if (results.length >= howMany) { - return results; - } - } - } - throw new Error(`can't make sequential uri with base ${base}`); - } - - nextNumberedResource(base: any) { - return this.nextNumberedResources(base, 1)[0]; - } - - contextsWithPattern(s: any, p: any, o: any) { - this._autoDeps.askedFor(s, p, o, null); - const ctxs = []; - for (let q of Array.from(this.graph.getQuads(s, p, o, null))) { - ctxs.push(q.graph); - } - return unique(ctxs); - } - - sortKey(uri: N3.NamedNode) { - const parts = uri.value.split(/([0-9]+)/); - const expanded = parts.map(function (p: string) { - const f = parseInt(p); - if (isNaN(f)) { - return p; - } - return p.padStart(8, "0"); - }); - return expanded.join(""); - } - - sortedUris(uris: any) { - return sortBy(uris, this.sortKey); - } - - prettyLiteral(x: any) { - if (typeof x === "number") { - return this.LiteralRoundedFloat(x); - } else { - return this.Literal(x); - } - } -}
--- a/light9/web/rdfdb-synced-graph.html Sun May 22 03:04:18 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -<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> - -<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') - } - - 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>
--- a/light9/web/rdfdbclient.ts Sun May 22 03:04:18 2022 -0700 +++ b/light9/web/rdfdbclient.ts Mon May 23 23:32:37 2022 -0700 @@ -9,10 +9,10 @@ _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 @@ -30,7 +30,6 @@ this._patchesReceived = 0; this._patchesSent = 0; this._connectionId = "??"; - this._reconnectionTimeout = null; this.ws = undefined; this._newConnection(); @@ -63,7 +62,7 @@ } 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(); @@ -76,35 +75,37 @@ 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() { @@ -115,7 +116,7 @@ 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; } } @@ -131,6 +132,7 @@ if (input.connectedAs) { this._connectionId = input.connectedAs; } else { + log("patch from server [0]") parseJsonPatch(input, this.applyPatch.bind(this)); this._patchesReceived++; } @@ -148,7 +150,7 @@ 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"); }
--- a/light9/web/resource-display.html Sun May 22 03:04:18 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,158 +0,0 @@ -<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"> - -<dom-module id="resource-display"> - <template> - <style> - :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> - - <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> - - <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>