Mercurial > code > home > repos > light9
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 } |