Mercurial > code > home > repos > light9
diff web/patch.ts @ 2376:4556eebe5d73
topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author | drewp@bigasterisk.com |
---|---|
date | Sun, 12 May 2024 19:02:10 -0700 |
parents | light9/web/patch.ts@cdfd2901918a |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/patch.ts Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,217 @@ +import * as async from "async"; +import debug from "debug"; +import * as N3 from "n3"; +import { NamedNode, Parser, Quad, Writer } from "n3"; +import * as Immutable from "immutable"; +export interface QuadPattern { + subject: N3.Quad_Subject | null; + predicate: N3.Quad_Predicate | null; + object: N3.Quad_Object | null; // literals allowed? needs review. probably 'yes'. + graph: N3.Quad_Graph | null; +} + +const log = debug("patch"); + +export class Patch { + // immutable + private dels: Immutable.Set<Quad>; + private adds: Immutable.Set<Quad>; + private _allPredsCache?: Immutable.Set<string>; + private _allSubjsCache?: Immutable.Set<string>; + constructor(dels: Iterable<Quad>, adds: Iterable<Quad>) { + this.dels = Immutable.Set(dels); + this.adds = Immutable.Set(adds); + this.validate(); + } + + private validate() { + // todo: finish porting this from coffeescript + this.adds.union(this.dels).forEach((q: Quad) => { + 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)}`); + } + if ( + q.object.termType == "Literal" && + (q.object.datatypeString == "http://www.w3.org/2001/XMLSchema#float" || q.object.datatypeString == "http://www.w3.org/2001/XMLSchema#double") + ) { + throw new Error(`${JSON.stringify(q)} is using non-decimal for numbers, which is going to break some comparisons`); + } + }); + } + + matches(pat: QuadPattern): boolean { + const allQuads = this.dels.concat(this.adds); + return allQuads.some((quad) => { + return ( + (pat.subject === null || pat.subject.equals(quad.subject)) && // + (pat.predicate === null || pat.predicate.equals(quad.predicate)) && // + (pat.object === null || pat.object.equals(quad.object)) && // + (pat.graph === null || pat.graph.equals(quad.graph)) + ); + }); + } + + isEmpty() { + return Immutable.is(this.dels, this.adds); + } + + applyToGraph(g: N3.Store) { + for (let quad of this.dels) { + g.removeQuad(quad); + } + for (let quad of this.adds) { + g.addQuad(quad); + } + } + + update(other: Patch): Patch { + // this is approx, since it doesnt handle cancelling existing quads. + return new Patch(this.dels.union(other.dels), this.adds.union(other.adds)); + } + + summary(): string { + return "-" + this.dels.size + " +" + this.adds.size; + } + + dump(): string { + if (this.dels.size + this.adds.size > 20) { + return this.summary(); + } + const lines: string[] = []; + const s = (term: N3.Term): string => { + if (term.termType == "Literal") return term.value; + if (term.termType == "NamedNode") + return term.value + .replace("http://light9.bigasterisk.com/effect/", "effect:") + .replace("http://light9.bigasterisk.com/", ":") + .replace("http://www.w3.org/2000/01/rdf-schema#", "rdfs:") + .replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:"); + if (term.termType == "BlankNode") return "_:" + term.value; + return term.id; + }; + const delPrefix = "- ", + addPrefix = "\u200B+ "; // dels to sort before adds + this.dels.forEach((d) => lines.push(delPrefix + s(d.subject) + " " + s(d.predicate) + " " + s(d.object))); + this.adds.forEach((d) => lines.push(addPrefix + s(d.subject) + " " + s(d.predicate) + " " + s(d.object))); + lines.sort(); + return lines.join("\n") + "\n" + (this.isEmpty() ? "(empty)" : "(nonempty)"); + } + + async toJsonPatch(): Promise<string> { + 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.toArray()); + 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.toArray()); + writer.end(function (err: any, result: string) { + out.patch.adds = result; + cb2(); + }); + }; + + async.parallel([writeDels, writeAdds], (err: any) => res(JSON.stringify(out))); + }); + } + + containsAnyPreds(preds: Iterable<NamedNode>): boolean { + if (this._allPredsCache === undefined) { + this._allPredsCache = Immutable.Set(); + this._allPredsCache.withMutations((cache) => { + for (let qq of [this.adds, this.dels]) { + for (let q of Array.from(qq)) { + cache.add(q.predicate.value); + } + } + }); + } + + for (let p of preds) { + if (this._allPredsCache.has(p.value)) { + return true; + } + } + return false; + } + + allSubjs(): Immutable.Set<string> { + // returns subjs as Set of strings + if (this._allSubjsCache === undefined) { + this._allSubjsCache = Immutable.Set(); + this._allSubjsCache.withMutations((cache) => { + for (let qq of [this.adds, this.dels]) { + for (let q of Array.from(qq)) { + cache.add(q.subject.value); + } + } + }); + } + + return this._allSubjsCache; + } + + allPreds(): Immutable.Set<NamedNode> { + // todo: this could cache + const ret = Immutable.Set<NamedNode>(); + ret.withMutations((r) => { + for (let qq of [this.adds, this.dels]) { + for (let q of Array.from(qq)) { + if (q.predicate.termType == "Variable") throw "unsupported"; + r.add(q.predicate); + } + } + }); + return ret; + } +} + +// The schema of the json sent from graph server. +export interface SyncgraphPatchMessage { + patch: { adds: string; deletes: string }; +} + +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 dels: Quad[] = []; + const adds: Quad[] = []; + + const parseAdds = (cb2: () => any) => { + const parser = new Parser(); + return parser.parse(input.patch.adds, (error: any, quad: Quad, prefixes: any) => { + if (quad) { + return adds.push(quad); + } else { + return cb2(); + } + }); + }; + const parseDels = (cb3: () => any) => { + const parser = new Parser(); + return parser.parse(input.patch.deletes, (error: any, quad: any, prefixes: any) => { + if (quad) { + return dels.push(quad); + } else { + return cb3(); + } + }); + }; + + // todo: is it faster to run them in series? might be + async.parallel([parseAdds, parseDels], (err: any) => cb(new Patch(dels, adds))); +}