comparison 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
comparison
equal deleted inserted replaced
2375:623836db99af 2376:4556eebe5d73
1 import * as async from "async";
2 import debug from "debug";
3 import * as N3 from "n3";
4 import { NamedNode, Parser, Quad, Writer } from "n3";
5 import * as Immutable from "immutable";
6 export interface QuadPattern {
7 subject: N3.Quad_Subject | null;
8 predicate: N3.Quad_Predicate | null;
9 object: N3.Quad_Object | null; // literals allowed? needs review. probably 'yes'.
10 graph: N3.Quad_Graph | null;
11 }
12
13 const log = debug("patch");
14
15 export class Patch {
16 // immutable
17 private dels: Immutable.Set<Quad>;
18 private adds: Immutable.Set<Quad>;
19 private _allPredsCache?: Immutable.Set<string>;
20 private _allSubjsCache?: Immutable.Set<string>;
21 constructor(dels: Iterable<Quad>, adds: Iterable<Quad>) {
22 this.dels = Immutable.Set(dels);
23 this.adds = Immutable.Set(adds);
24 this.validate();
25 }
26
27 private validate() {
28 // todo: finish porting this from coffeescript
29 this.adds.union(this.dels).forEach((q: Quad) => {
30 if (!q.equals) {
31 throw new Error("doesn't look like a proper Quad");
32 }
33 if (!q.subject.id || q.graph.id == null || q.predicate.id == null) {
34 throw new Error(`corrupt patch: ${JSON.stringify(q)}`);
35 }
36 if (
37 q.object.termType == "Literal" &&
38 (q.object.datatypeString == "http://www.w3.org/2001/XMLSchema#float" || q.object.datatypeString == "http://www.w3.org/2001/XMLSchema#double")
39 ) {
40 throw new Error(`${JSON.stringify(q)} is using non-decimal for numbers, which is going to break some comparisons`);
41 }
42 });
43 }
44
45 matches(pat: QuadPattern): boolean {
46 const allQuads = this.dels.concat(this.adds);
47 return allQuads.some((quad) => {
48 return (
49 (pat.subject === null || pat.subject.equals(quad.subject)) && //
50 (pat.predicate === null || pat.predicate.equals(quad.predicate)) && //
51 (pat.object === null || pat.object.equals(quad.object)) && //
52 (pat.graph === null || pat.graph.equals(quad.graph))
53 );
54 });
55 }
56
57 isEmpty() {
58 return Immutable.is(this.dels, this.adds);
59 }
60
61 applyToGraph(g: N3.Store) {
62 for (let quad of this.dels) {
63 g.removeQuad(quad);
64 }
65 for (let quad of this.adds) {
66 g.addQuad(quad);
67 }
68 }
69
70 update(other: Patch): Patch {
71 // this is approx, since it doesnt handle cancelling existing quads.
72 return new Patch(this.dels.union(other.dels), this.adds.union(other.adds));
73 }
74
75 summary(): string {
76 return "-" + this.dels.size + " +" + this.adds.size;
77 }
78
79 dump(): string {
80 if (this.dels.size + this.adds.size > 20) {
81 return this.summary();
82 }
83 const lines: string[] = [];
84 const s = (term: N3.Term): string => {
85 if (term.termType == "Literal") return term.value;
86 if (term.termType == "NamedNode")
87 return term.value
88 .replace("http://light9.bigasterisk.com/effect/", "effect:")
89 .replace("http://light9.bigasterisk.com/", ":")
90 .replace("http://www.w3.org/2000/01/rdf-schema#", "rdfs:")
91 .replace("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:");
92 if (term.termType == "BlankNode") return "_:" + term.value;
93 return term.id;
94 };
95 const delPrefix = "- ",
96 addPrefix = "\u200B+ "; // dels to sort before adds
97 this.dels.forEach((d) => lines.push(delPrefix + s(d.subject) + " " + s(d.predicate) + " " + s(d.object)));
98 this.adds.forEach((d) => lines.push(addPrefix + s(d.subject) + " " + s(d.predicate) + " " + s(d.object)));
99 lines.sort();
100 return lines.join("\n") + "\n" + (this.isEmpty() ? "(empty)" : "(nonempty)");
101 }
102
103 async toJsonPatch(): Promise<string> {
104 return new Promise((res, rej) => {
105 const out: SyncgraphPatchMessage = { patch: { adds: "", deletes: "" } };
106
107 const writeDels = (cb1: () => void) => {
108 const writer = new Writer({ format: "N-Quads" });
109 writer.addQuads(this.dels.toArray());
110 writer.end(function (err: any, result: string) {
111 out.patch.deletes = result;
112 cb1();
113 });
114 };
115
116 const writeAdds = (cb2: () => void) => {
117 const writer = new Writer({ format: "N-Quads" });
118 writer.addQuads(this.adds.toArray());
119 writer.end(function (err: any, result: string) {
120 out.patch.adds = result;
121 cb2();
122 });
123 };
124
125 async.parallel([writeDels, writeAdds], (err: any) => res(JSON.stringify(out)));
126 });
127 }
128
129 containsAnyPreds(preds: Iterable<NamedNode>): boolean {
130 if (this._allPredsCache === undefined) {
131 this._allPredsCache = Immutable.Set();
132 this._allPredsCache.withMutations((cache) => {
133 for (let qq of [this.adds, this.dels]) {
134 for (let q of Array.from(qq)) {
135 cache.add(q.predicate.value);
136 }
137 }
138 });
139 }
140
141 for (let p of preds) {
142 if (this._allPredsCache.has(p.value)) {
143 return true;
144 }
145 }
146 return false;
147 }
148
149 allSubjs(): Immutable.Set<string> {
150 // returns subjs as Set of strings
151 if (this._allSubjsCache === undefined) {
152 this._allSubjsCache = Immutable.Set();
153 this._allSubjsCache.withMutations((cache) => {
154 for (let qq of [this.adds, this.dels]) {
155 for (let q of Array.from(qq)) {
156 cache.add(q.subject.value);
157 }
158 }
159 });
160 }
161
162 return this._allSubjsCache;
163 }
164
165 allPreds(): Immutable.Set<NamedNode> {
166 // todo: this could cache
167 const ret = Immutable.Set<NamedNode>();
168 ret.withMutations((r) => {
169 for (let qq of [this.adds, this.dels]) {
170 for (let q of Array.from(qq)) {
171 if (q.predicate.termType == "Variable") throw "unsupported";
172 r.add(q.predicate);
173 }
174 }
175 });
176 return ret;
177 }
178 }
179
180 // The schema of the json sent from graph server.
181 export interface SyncgraphPatchMessage {
182 patch: { adds: string; deletes: string };
183 }
184
185 export function patchToDeleteEntireGraph(g: N3.Store) {
186 return new Patch(g.getQuads(null, null, null, null), []);
187 }
188
189 export function parseJsonPatch(input: SyncgraphPatchMessage, cb: (p: Patch) => void): void {
190 // note response cb doesn't have an error arg.
191 const dels: Quad[] = [];
192 const adds: Quad[] = [];
193
194 const parseAdds = (cb2: () => any) => {
195 const parser = new Parser();
196 return parser.parse(input.patch.adds, (error: any, quad: Quad, prefixes: any) => {
197 if (quad) {
198 return adds.push(quad);
199 } else {
200 return cb2();
201 }
202 });
203 };
204 const parseDels = (cb3: () => any) => {
205 const parser = new Parser();
206 return parser.parse(input.patch.deletes, (error: any, quad: any, prefixes: any) => {
207 if (quad) {
208 return dels.push(quad);
209 } else {
210 return cb3();
211 }
212 });
213 };
214
215 // todo: is it faster to run them in series? might be
216 async.parallel([parseAdds, parseDels], (err: any) => cb(new Patch(dels, adds)));
217 }