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