# HG changeset patch # User drewp@bigasterisk.com # Date 1685385671 25200 # Node ID d3ecee9bfab5bf2aed7e1ca305c8e382c464f61b # Parent 8ebf79d6b957a5a774e8abc8024bbd90b1a46414 refactor Patch into a class diff -r 8ebf79d6b957 -r d3ecee9bfab5 light9/live/Effect.ts --- a/light9/live/Effect.ts Mon May 29 11:33:23 2023 -0700 +++ b/light9/live/Effect.ts Mon May 29 11:41:11 2023 -0700 @@ -1,7 +1,7 @@ import debug from "debug"; import { Literal, NamedNode, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3"; import { some } from "underscore"; -import { Patch, patchContainsPreds, patchUpdate } from "../web/patch"; +import { Patch } from "../web/patch"; import { SyncedGraph } from "../web/SyncedGraph"; import { shortShow } from "../web/show_specific"; import { SubEvent } from "sub-events"; @@ -50,7 +50,7 @@ quad(this.uri, U("rdfs:label"), this.graph.Literal(this.uri.value.replace(/.*\//, ""))), quad(this.uri, U(":publishAttr"), U(":strength")), ]; - const patch = { adds: addQuads, dels: [] } as Patch; + const patch = new Patch([], addQuads); log("init new effect", patch); this.settings = []; this.graph.applyAndSendPatch(patch); @@ -58,7 +58,7 @@ rebuildSettingsFromGraph(patch?: Patch) { const U = this.graph.U(); - if (patch && !patchContainsPreds(patch, [U(":setting"), U(":device"), U(":deviceAttr")])) { + if (patch && !patch.containsAnyPreds([U(":setting"), U(":device"), U(":deviceAttr")])) { // that's an approx list of preds , but it just means we'll miss some pathological settings edits // return; } @@ -111,13 +111,13 @@ edit(device: NamedNode, deviceAttr: NamedNode, newValue: ControlValue | null): Patch { log(`edit: value=${newValue}`); let existingSetting: NamedNode | null = null; - let result = { adds: [], dels: [] }; + let result = new Patch([], []); for (let s of this.settings) { if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) { if (existingSetting !== null) { // this is corrupt. There was only supposed to be one setting per (dev,attr) pair. But we can fix it because we're going to update existingSetting to the user's requested value. log(`${this.uri.value} had two settings for ${device.value} - ${deviceAttr.value} - deleting ${s.setting}`); - patchUpdate(result, this._removeEffectSetting(s.setting)); + result = result.update(this.removeEffectSetting(s.setting)); } existingSetting = s.setting; } @@ -125,13 +125,13 @@ if (newValue !== null && this.shouldBeStored(deviceAttr, newValue)) { if (existingSetting === null) { - patchUpdate(result, this._addEffectSetting(device, deviceAttr, newValue)); + result = result.update(this.addEffectSetting(device, deviceAttr, newValue)); } else { - patchUpdate(result, this._patchExistingEffectSetting(existingSetting, deviceAttr, newValue)); + result = result.update(this.patchExistingEffectSetting(existingSetting, deviceAttr, newValue)); } } else { if (existingSetting !== null) { - patchUpdate(result, this._removeEffectSetting(existingSetting)); + result = result.update(this.removeEffectSetting(existingSetting)); } } return result; @@ -152,19 +152,21 @@ if (!this.uri) throw new Error("effect unset"); const setting = this.graph.nextNumberedResource(this.uri.value + "_set"); - const addQuads = [ - quad(this.uri, U(":setting"), setting), - quad(setting, U(":device"), device), - quad(setting, U(":deviceAttr"), deviceAttr), - quad(setting, valuePred(this.graph, deviceAttr), this._nodeForValue(value)), - ]; - const patch = { adds: addQuads, dels: [] } as Patch; + const patch = new Patch( + [], + [ + quad(this.uri, U(":setting"), setting), + quad(setting, U(":device"), device), + quad(setting, U(":deviceAttr"), deviceAttr), + quad(setting, valuePred(this.graph, deviceAttr), this.nodeForValue(value)), + ] + ); log(" save", patch); this.settings.push({ device, deviceAttr, setting, value }); return patch; } - _patchExistingEffectSetting(effectSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch { + private patchExistingEffectSetting(effectSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch { log(" patch existing", effectSetting.value); return this.graph.getObjectPatch( effectSetting, // @@ -181,7 +183,7 @@ for (let q of this.graph.subjectStatements(effectSetting)) { toDel.push(q); } - return { dels: toDel, adds: [] }; + return new Patch(toDel, []); } _nodeForValue(value: ControlValue): NamedNode | Literal { diff -r 8ebf79d6b957 -r d3ecee9bfab5 light9/live/Light9DeviceControl.ts --- a/light9/live/Light9DeviceControl.ts Mon May 29 11:33:23 2023 -0700 +++ b/light9/live/Light9DeviceControl.ts Mon May 29 11:41:11 2023 -0700 @@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators.js"; import { NamedNode } from "n3"; import { unique } from "underscore"; -import { Patch, patchContainsPreds } from "../web/patch"; +import { Patch } from "../web/patch"; import { getTopGraph } from "../web/RdfdbSyncedGraph"; import { SyncedGraph } from "../web/SyncedGraph"; import { Choice } from "./Light9Listbox"; @@ -142,7 +142,7 @@ syncDeviceAttrsFromGraph(patch?: Patch) { const U = this.graph.U(); - if (patch && !patchContainsPreds(patch, [U("rdf:type"), U(":deviceAttr"), U(":dataType"), U(":choice")])) { + if (patch && !patch.containsAnyPreds([U("rdf:type"), U(":deviceAttr"), U(":dataType"), U(":choice")])) { return; } try { diff -r 8ebf79d6b957 -r d3ecee9bfab5 light9/web/AutoDependencies.ts --- a/light9/web/AutoDependencies.ts Mon May 29 11:33:23 2023 -0700 +++ b/light9/web/AutoDependencies.ts Mon May 29 11:41:11 2023 -0700 @@ -1,7 +1,7 @@ import debug from "debug"; -import { Quad_Graph, Quad_Object, Quad_Predicate, Quad_Subject } from "n3"; +import { NamedNode, Quad_Graph, Quad_Object, Quad_Predicate, Quad_Subject, Term, Util } from "n3"; import { filter } from "underscore"; -import { allPatchSubjs, Patch } from "./patch"; +import { Patch, QuadPattern } from "./patch"; const log = debug("autodep"); @@ -52,7 +52,7 @@ // 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. - this._rerunHandler(h, undefined); + this._rerunHandler(h, /*patch=*/ undefined); } //console.timeEnd("handler #{label}") //@_logHandlerTree() diff -r 8ebf79d6b957 -r d3ecee9bfab5 light9/web/ResourceDisplay.ts --- a/light9/web/ResourceDisplay.ts Mon May 29 11:33:23 2023 -0700 +++ b/light9/web/ResourceDisplay.ts Mon May 29 11:41:11 2023 -0700 @@ -4,7 +4,7 @@ import { NamedNode } from "n3"; // import { GraphChangedEvent } from "../../web/RdfdbSyncedGraph"; // import { runHandler } from "./GraphAwarePage"; -import { Patch, patchContainsPreds, patchSizeSummary } from "./patch"; +import { Patch } from "./patch"; import { getTopGraph } from "./RdfdbSyncedGraph"; import { SyncedGraph } from "./SyncedGraph"; debug.enable("*"); @@ -116,12 +116,11 @@ } runUriHandler() { - this.graph.runHandler(this.onUri.bind(this), `rdisplay ${this.href()}` /*needs uniqueness?*/); } onUri(patch?: Patch) { - if (patch && !patchContainsPreds(patch, [this.graph.Uri("rdfs:label")])) { + if (patch && !patch.containsAnyPreds([RDFS_LABEL])) { return; } @@ -135,7 +134,7 @@ } setLabel(patch?: Patch) { - if (patch && !patchContainsPreds(patch, [RDFS_LABEL])) { + if (patch && !patch.containsAnyPreds([RDFS_LABEL])) { return; } const uri = this.realUri(); diff -r 8ebf79d6b957 -r d3ecee9bfab5 light9/web/SyncedGraph.ts --- a/light9/web/SyncedGraph.ts Mon May 29 11:33:23 2023 -0700 +++ b/light9/web/SyncedGraph.ts Mon May 29 11:41:11 2023 -0700 @@ -3,7 +3,7 @@ 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 { Patch, patchToDeleteEntireGraph } from "./patch"; import { RdfDbClient } from "./rdfdbclient"; const log = debug("graph"); @@ -47,7 +47,7 @@ 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) }); + this._applyPatch(patchToDeleteEntireGraph(this.graph)); // if we had a Store already, this lets N3.Store free all its indices/etc this.graph = new N3.Store(); this.rebuildPrefixFuncs(this.prefixes); @@ -115,19 +115,19 @@ loadTrig(trig: any, cb: () => any) { // for debugging - const patch: Patch = { dels: [], adds: [] }; + const adds: Quad[] = []; const parser = new N3.Parser(); - return parser.parse(trig, (error: any, quad: any, prefixes: any) => { + parser.parse(trig, (error: any, quad: any, prefixes: any) => { if (error) { throw new Error(error); } if (quad) { - return patch.adds.push(quad); + adds.push(quad); } else { - this._applyPatch(patch); + this._applyPatch(new Patch([], adds)); // todo: here, add those prefixes to our known set if (cb) { - return cb(); + cb(); } } }); @@ -144,13 +144,6 @@ 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) { @@ -159,25 +152,6 @@ 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. // @@ -185,26 +159,16 @@ 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); + patch.applyToGraph(this.graph); + log("applied patch locally", patch.summary()); + this.autoDeps.graphChanged(patch); } getObjectPatch(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object | null, 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: newObject !== null ? [this.Quad(s, p, newObject, g)] : [], - }; + return new Patch(existing, newObject !== null ? [this.Quad(s, p, newObject, g)] : []); } patchObject(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object | null, g: N3.NamedNode) { @@ -212,10 +176,7 @@ } clearObjects(s: N3.NamedNode, p: N3.NamedNode, g: N3.NamedNode) { - return this.applyAndSendPatch({ - dels: this.graph.getQuads(s, p, null, g), - adds: [], - }); + this.applyAndSendPatch(new Patch(this.graph.getQuads(s, p, null, g), [])); } public runHandler(func: HandlerFunc, label: string) { diff -r 8ebf79d6b957 -r d3ecee9bfab5 light9/web/patch.ts --- a/light9/web/patch.ts Mon May 29 11:33:23 2023 -0700 +++ b/light9/web/patch.ts Mon May 29 11:41:11 2023 -0700 @@ -1,110 +1,184 @@ import * as async from "async"; import debug from "debug"; +import * as N3 from "n3"; import { NamedNode, Parser, Quad, Writer } from "n3"; const log = debug("patch"); -export interface Patch { - dels: Quad[]; - adds: Quad[]; - _allPredsCache?: Set; - _allSubjsCache?: Set; +export class Patch { + // immutable + private _allPredsCache?: Set; + private _allSubjsCache?: Set; + constructor(private dels: Quad[], private adds: Quad[]) { + this.validate(); + } + private validate() { + // todo: finish porting this from coffeescript + [this.adds, this.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; + })() + ); + } + + isEmpty() { + return !this.dels.length && !this.adds.length; + } + + applyToGraph(g: N3.Store) { + for (let quad of Array.from(this.dels)) { + g.removeQuad(quad); + } + for (let quad of Array.from(this.adds)) { + g.addQuad(quad); + } + } + + update(other: Patch): Patch { + // this is approx, since it doesnt handle matching existing quads. + return new Patch(this.dels.concat(other.dels), this.adds.concat(other.adds)); + } + + summary(): string { + return "-" + this.dels.length + " +" + this.adds.length; + } + + async toJsonPatch(): Promise { + return new Promise((res, rej) => { + const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } }; + + const writeDels = (cb1: () => void) => { + const writer = new Writer({ format: "N-Quads" }); + writer.addQuads(this.dels); + writer.end(function (err: any, result: string) { + out.patch.deletes = result; + cb1(); + }); + }; + + const writeAdds = (cb2: () => void) => { + const writer = new Writer({ format: "N-Quads" }); + writer.addQuads(this.adds); + writer.end(function (err: any, result: string) { + out.patch.adds = result; + cb2(); + }); + }; + + async.parallel([writeDels, writeAdds], (err: any) => res(JSON.stringify(out))); + }); + } + + containsAnyPreds(preds: NamedNode[]): boolean { + if (this._allPredsCache === undefined) { + this._allPredsCache = new Set(); + for (let qq of [this.adds, this.dels]) { + for (let q of Array.from(qq)) { + this._allPredsCache.add(q.predicate.value); + } + } + } + + for (let p of Array.from(preds)) { + if (this._allPredsCache.has(p.value)) { + return true; + } + } + return false; + } + + allSubjs(): Set { + // returns subjs as Set of strings + if (this._allSubjsCache === undefined) { + this._allSubjsCache = new Set(); + for (let qq of [this.adds, this.dels]) { + for (let q of Array.from(qq)) { + this._allSubjsCache.add(q.subject.value); + } + } + } + + return this._allSubjsCache; + } + allPreds(): Set { + const ret = new Set(); + for (let qq of [this.adds, this.dels]) { + for (let q of Array.from(qq)) { + if (q.predicate.termType == "Variable") throw "unsupported"; + ret.add(q.predicate); + } + } + return ret; + } } -interface SyncgraphPatchMessage { +export interface SyncgraphPatchMessage { patch: { adds: string; deletes: string }; } -export function patchUpdate(p1: Patch, p2: Patch): void { - // this is approx, since it doesnt handle matching existing quads. - p1.adds = p1.adds.concat(p2.adds); - p1.dels = p1.dels.concat(p2.dels); -} - -export function patchSizeSummary(patch: Patch) { - return "-" + patch.dels.length + " +" + patch.adds.length; +export function patchToDeleteEntireGraph(g: N3.Store) { + return new Patch(g.getQuads(null, null, null, null), []); } export function parseJsonPatch(input: SyncgraphPatchMessage, cb: (p: Patch) => void): void { // note response cb doesn't have an error arg. - const patch: Patch = { dels: [], adds: [] }; + const dels: Quad[] = []; + const adds: Quad[] = []; - const parseAdds = (cb: () => any) => { + const parseAdds = (cb2: () => any) => { const parser = new Parser(); return parser.parse(input.patch.adds, (error: any, quad: Quad, prefixes: any) => { if (quad) { - return patch.adds.push(quad); + return adds.push(quad); } else { - return cb(); + return cb2(); } }); }; - const parseDels = (cb: () => any) => { + const parseDels = (cb3: () => any) => { const parser = new Parser(); return parser.parse(input.patch.deletes, (error: any, quad: any, prefixes: any) => { if (quad) { - return patch.dels.push(quad); + return dels.push(quad); } else { - return cb(); + return cb3(); } }); }; // todo: is it faster to run them in series? might be - async.parallel([parseAdds, parseDels], (err: any) => cb(patch)); -} - -export function toJsonPatch(jsPatch: Patch, cb: (jsonString: string) => void): void { - const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } }; - - const writeDels = function (cb: () => any) { - const writer = new Writer({ format: "N-Quads" }); - writer.addQuads(jsPatch.dels); - return writer.end(function (err: any, result: string) { - out.patch.deletes = result; - return cb(); - }); - }; - - const writeAdds = function (cb: () => any) { - const writer = new Writer({ format: "N-Quads" }); - writer.addQuads(jsPatch.adds); - return writer.end(function (err: any, result: string) { - out.patch.adds = result; - return cb(); - }); - }; - - async.parallel([writeDels, writeAdds], (err: any) => cb(JSON.stringify(out))); + async.parallel([parseAdds, parseDels], (err: any) => cb(new Patch(dels, adds))); } -export function patchContainsPreds(patch: Patch, preds: NamedNode[]): boolean { - if (patch._allPredsCache === undefined) { - patch._allPredsCache = new Set(); - for (let qq of [patch.adds, patch.dels]) { - for (let q of Array.from(qq)) { - patch._allPredsCache.add(q.predicate.value); - } - } - } +// /** @deprecated replace with p=p.update(p2) */ +// export function patchUpdate(p1: Patch, p2: Patch): void { +// throw ""; +// } + +// /** @deprecated replace with p.summary() */ +// export function patchSizeSummary(patch: Patch) { +// throw ""; +// } - for (let p of Array.from(preds)) { - if (patch._allPredsCache.has(p.value)) { - return true; - } - } - return false; -} - -export function allPatchSubjs(patch: Patch): Set { - // returns subjs as Set of strings - if (patch._allSubjsCache === undefined) { - patch._allSubjsCache = new Set(); - for (let qq of [patch.adds, patch.dels]) { - for (let q of Array.from(qq)) { - patch._allSubjsCache.add(q.subject.value); - } - } - } - - return patch._allSubjsCache; -} +// /** @deprecated moved to Patch.toJsonPatch */ +// export function toJsonPatch(jsPatch: Patch, cb: (jsonString: string) => void): void { +// throw ""; +// } +// /** @deprecated moved to Patch.containsAnyPreds */ +// export function patchContainsPreds(patch: Patch, preds: NamedNode[]): boolean { +// throw ""; +// } +// /** @deprecated moved to Patch.allSubjs */ +// export function allPatchSubjs(patch: Patch): Set { +// throw ""; +// } diff -r 8ebf79d6b957 -r d3ecee9bfab5 light9/web/rdfdbclient.ts --- a/light9/web/rdfdbclient.ts Mon May 29 11:33:23 2023 -0700 +++ b/light9/web/rdfdbclient.ts Mon May 29 11:41:11 2023 -0700 @@ -1,5 +1,5 @@ import debug from "debug"; -import { parseJsonPatch, Patch, patchSizeSummary, toJsonPatch } from "./patch"; +import { parseJsonPatch, Patch } from "./patch"; import { RdfDbChannel } from "./RdfDbChannel"; export const log = debug("rdfdbclient"); @@ -33,7 +33,7 @@ } sendPatch(patch: Patch) { - log("queue patch to server ", patchSizeSummary(patch)); + log("queue patch to server ", patch.summary()); this._patchesToSend.push(patch); this._continueSending(); } @@ -47,9 +47,7 @@ // the dragging cases. while (this._patchesToSend.length) { const patch = this._patchesToSend.splice(0, 1)[0]; - const json = await new Promise((res, rej) => { - toJsonPatch(patch, res); - }); + const json = await patch.toJsonPatch(); const ret = this.channel.sendMessage(json); if (!ret) { setTimeout(this._continueSending.bind(this), 500);