comparison web/SyncedGraph.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/SyncedGraph.ts@855f1abf5c66
children ac55319a2eac
comparison
equal deleted inserted replaced
2375:623836db99af 2376:4556eebe5d73
1 import debug from "debug";
2 import * as N3 from "n3";
3 import { Quad, Quad_Object, Quad_Predicate, Quad_Subject } from "n3";
4 import { sortBy, unique } from "underscore";
5 import { AutoDependencies, HandlerFunc } from "./AutoDependencies";
6 import { Patch, patchToDeleteEntireGraph } from "./patch";
7 import { RdfDbClient } from "./rdfdbclient";
8
9 const log = debug("graph");
10
11 const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
12
13 export class SyncedGraph {
14 private autoDeps: AutoDependencies;
15 private client: RdfDbClient;
16 private graph: N3.Store;
17 private cachedFloatValues: Map<string, number> = new Map();
18 private cachedUriValues: Map<string, N3.NamedNode> = new Map();
19 private prefixFuncs: (prefix: string) => N3.PrefixedToIri;
20 private serial: any;
21 private nextNumber: any;
22 // Main graph object for a browser to use. Consider using RdfdbSyncedGraph element to create & own
23 // one of these. Syncs both ways with rdfdb. Meant to hide the choice of RDF lib, so we can change it
24 // later.
25 //
26 // Note that _applyPatch is the only method to write to the graph, so
27 // it can fire subscriptions.
28
29 constructor(
30 // The /syncedGraph path of an rdfdb server.
31 patchSenderUrl: string,
32 // prefixes can be used in Uri(curie) calls. This mapping may grow during loadTrig calls.
33 public prefixes: Map<string, string>,
34 private setStatus: (status: string) => void
35 ) {
36 this.prefixFuncs = this.rebuildPrefixFuncs(prefixes);
37 this.graph = new N3.Store();
38 this.autoDeps = new AutoDependencies(this);
39 this.autoDeps.graphError.subscribe((e) => {
40 log("graph learned of error - reconnecting", e);
41 this.client.disconnect("graph error");
42 });
43 this.clearGraph();
44
45 this.client = new RdfDbClient(patchSenderUrl, this._clearGraphOnNewConnection.bind(this), this._applyPatch.bind(this), this.setStatus);
46 }
47
48 clearGraph() {
49 // must not try send a patch to the server!
50 // just deletes the statements; watchers are unaffected.
51 this.cachedFloatValues = new Map(); // s + '|' + p -> number
52 this.cachedUriValues = new Map(); // s + '|' + p -> Uri
53
54 const p = patchToDeleteEntireGraph(this.graph);
55 if (!p.isEmpty()) {
56 this._applyPatch(p);
57 }
58 // if we had a Store already, this lets N3.Store free all its indices/etc
59 this.graph = new N3.Store();
60 this.rebuildPrefixFuncs(this.prefixes);
61 }
62
63 _clearGraphOnNewConnection() {
64 // must not try send a patch to the server
65
66 log("clearGraphOnNewConnection");
67 this.clearGraph();
68 log("clearGraphOnNewConnection done");
69 }
70
71 private rebuildPrefixFuncs(prefixes: Map<string, string>) {
72 const p = Object.create(null);
73 prefixes.forEach((v: string, k: string) => (p[k] = v));
74
75 this.prefixFuncs = N3.Util.prefixes(p);
76 return this.prefixFuncs;
77 }
78
79 U() {
80 // just a shorthand
81 return this.Uri.bind(this);
82 }
83
84 Uri(curie: string) {
85 if (curie == null) {
86 throw new Error("no uri");
87 }
88 if (curie.match(/^http/)) {
89 return N3.DataFactory.namedNode(curie);
90 }
91 const part = curie.split(":");
92 return this.prefixFuncs(part[0])(part[1]);
93 }
94
95 // Uri(shorten(u)).value==u
96 shorten(uri: N3.NamedNode): string {
97 for (let row of [
98 { sh: "dev", lo: "http://light9.bigasterisk.com/theater/vet/device/" },
99 { sh: "effect", lo: "http://light9.bigasterisk.com/effect/" },
100 { sh: "", lo: "http://light9.bigasterisk.com/" },
101 { sh: "rdfs", lo: "http://www.w3.org/2000/01/rdf-schema#" },
102 { sh: "xsd", lo: "http://www.w3.org/2001/XMLSchema#" },
103 ]) {
104 if (uri.value.startsWith(row.lo)) {
105 return row.sh + ":" + uri.value.substring(row.lo.length);
106 }
107 }
108 return uri.value;
109 }
110
111 Literal(jsValue: string | number) {
112 return N3.DataFactory.literal(jsValue);
113 }
114
115 LiteralRoundedFloat(f: number) {
116 return N3.DataFactory.literal(f.toPrecision(3), this.Uri("http://www.w3.org/2001/XMLSchema#decimal"));
117 }
118
119 Quad(s: any, p: any, o: any, g: any) {
120 return N3.DataFactory.quad(s, p, o, g);
121 }
122
123 toJs(literal: { value: any }) {
124 // incomplete
125 return parseFloat(literal.value);
126 }
127
128 loadTrig(trig: any, cb: () => any) {
129 // for debugging
130 const adds: Quad[] = [];
131 const parser = new N3.Parser();
132 parser.parse(trig, (error: any, quad: any, prefixes: any) => {
133 if (error) {
134 throw new Error(error);
135 }
136 if (quad) {
137 adds.push(quad);
138 } else {
139 this._applyPatch(new Patch([], adds));
140 // todo: here, add those prefixes to our known set
141 if (cb) {
142 cb();
143 }
144 }
145 });
146 }
147
148 quads(): any {
149 // for debugging
150 return Array.from(this.graph.getQuads(null, null, null, null)).map((q: Quad) => [q.subject, q.predicate, q.object, q.graph]);
151 }
152
153 applyAndSendPatch(patch: Patch) {
154 console.time("applyAndSendPatch");
155 if (!this.client) {
156 log("not connected-- dropping patch");
157 return;
158 }
159 if (!patch.isEmpty()) {
160 this._applyPatch(patch);
161 // // chaos delay
162 // setTimeout(()=>{
163 if (this.client) {
164 log("sending patch:\n", patch.dump());
165 this.client.sendPatch(patch);
166 }
167 // },300*Math.random())
168 }
169 console.timeEnd("applyAndSendPatch");
170 }
171
172 _applyPatch(patch: Patch) {
173 // In most cases you want applyAndSendPatch.
174 //
175 // This is the only method that writes to this.graph!
176 if (patch.isEmpty()) throw "dont send empty patches here";
177 log("_applyPatch [1] \n", patch.dump());
178 this.cachedFloatValues.clear();
179 this.cachedUriValues.clear();
180 patch.applyToGraph(this.graph);
181 if (false) {
182 log("applied patch locally", patch.summary());
183 } else {
184 log("applied patch locally:\n" + patch.dump());
185 }
186 this.autoDeps.graphChanged(patch);
187 }
188
189 getObjectPatch(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object | null, g: N3.NamedNode): Patch {
190 // make a patch which removes existing values for (s,p,*,c) and
191 // adds (s,p,newObject,c). Values in other graphs are not affected.
192 const existing = this.graph.getQuads(s, p, null, g);
193 return new Patch(existing, newObject !== null ? [this.Quad(s, p, newObject, g)] : []);
194 }
195
196 patchObject(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object | null, g: N3.NamedNode) {
197 this.applyAndSendPatch(this.getObjectPatch(s, p, newObject, g));
198 }
199
200 clearObjects(s: N3.NamedNode, p: N3.NamedNode, g: N3.NamedNode) {
201 this.applyAndSendPatch(new Patch(this.graph.getQuads(s, p, null, g), []));
202 }
203
204 public runHandler(func: HandlerFunc, label: string) {
205 // runs your func once, tracking graph calls. if a future patch
206 // matches what you queried, we runHandler your func again (and
207 // forget your queries from the first time).
208
209 // helps with memleak? not sure yet. The point was if two matching
210 // labels get puushed on, we should run only one. So maybe
211 // appending a serial number is backwards.
212 if (!this.serial) {
213 this.serial = 1;
214 }
215 this.serial += 1;
216 //label = label + @serial
217
218 this.autoDeps.runHandler(func, label);
219 }
220
221 _singleValue(s: Quad_Subject, p: Quad_Predicate) {
222 this.autoDeps.askedFor(s, p, null, null);
223 const quads = this.graph.getQuads(s, p, null, null);
224 const objs = new Set(Array.from(quads).map((q: Quad) => q.object));
225
226 switch (objs.size) {
227 case 0:
228 throw new Error("no value for " + s.value + " " + p.value);
229 case 1:
230 var obj = objs.values().next().value;
231 return obj;
232 default:
233 throw new Error("too many different values: " + JSON.stringify(quads));
234 }
235 }
236
237 floatValue(s: Quad_Subject, p: Quad_Predicate) {
238 const key = s.value + "|" + p.value;
239 const hit = this.cachedFloatValues.get(key);
240 if (hit !== undefined) {
241 return hit;
242 }
243 //log('float miss', s, p)
244
245 const v = this._singleValue(s, p).value;
246 const ret = parseFloat(v);
247 if (isNaN(ret)) {
248 throw new Error(`${s.value} ${p.value} -> ${v} not a float`);
249 }
250 this.cachedFloatValues.set(key, ret);
251 return ret;
252 }
253
254 stringValue(s: any, p: any) {
255 return this._singleValue(s, p).value;
256 }
257
258 uriValue(s: Quad_Subject, p: Quad_Predicate) {
259 const key = s.value + "|" + p.value;
260 const hit = this.cachedUriValues.get(key);
261 if (hit !== undefined) {
262 return hit;
263 }
264
265 const ret = this._singleValue(s, p);
266 this.cachedUriValues.set(key, ret);
267 return ret;
268 }
269
270 labelOrTail(uri: { value: { split: (arg0: string) => any } }) {
271 let ret: any;
272 try {
273 ret = this.stringValue(uri, this.Uri("rdfs:label"));
274 } catch (error) {
275 const words = uri.value.split("/");
276 ret = words[words.length - 1];
277 }
278 if (!ret) {
279 ret = uri.value;
280 }
281 return ret;
282 }
283
284 objects(s: any, p: any): Quad_Object[] {
285 this.autoDeps.askedFor(s, p, null, null);
286 const quads = this.graph.getQuads(s, p, null, null);
287 return Array.from(quads).map((q: { object: any }) => q.object);
288 }
289
290 subjects(p: any, o: any): Quad_Subject[] {
291 this.autoDeps.askedFor(null, p, o, null);
292 const quads = this.graph.getQuads(null, p, o, null);
293 return Array.from(quads).map((q: { subject: any }) => q.subject);
294 }
295
296 subjectStatements(s: Quad_Subject): Quad[] {
297 this.autoDeps.askedFor(s, null, null, null);
298 const quads = this.graph.getQuads(s, null, null, null);
299 return quads;
300 }
301
302 items(list: any) {
303 const out = [];
304 let current = list;
305 while (true) {
306 if (current.value === RDF + "nil") {
307 break;
308 }
309
310 this.autoDeps.askedFor(current, null, null, null); // a little loose
311
312 const firsts = this.graph.getQuads(current, RDF + "first", null, null);
313 const rests = this.graph.getQuads(current, RDF + "rest", null, null);
314 if (firsts.length !== 1) {
315 throw new Error(`list node ${current} has ${firsts.length} rdf:first edges`);
316 }
317 out.push(firsts[0].object);
318
319 if (rests.length !== 1) {
320 throw new Error(`list node ${current} has ${rests.length} rdf:rest edges`);
321 }
322 current = rests[0].object;
323 }
324
325 return out;
326 }
327
328 contains(s: any, p: any, o: any): boolean {
329 this.autoDeps.askedFor(s, p, o, null);
330 // Sure this is a nice warning to remind me to rewrite, but the graph.size call itself was taking 80% of the time in here
331 // log("contains calling getQuads when graph has ", this.graph.size);
332 return this.graph.getQuads(s, p, o, null).length > 0;
333 }
334
335 nextNumberedResources(base: { id: any }, howMany: number) {
336 // base is NamedNode or string
337 // Note this is unsafe before we're synced with the graph. It'll
338 // always return 'name0'.
339 if (base.id) {
340 base = base.id;
341 }
342 const results = [];
343
344 // @contains is really slow.
345 if (this.nextNumber == null) {
346 this.nextNumber = new Map();
347 }
348 let start = this.nextNumber.get(base);
349 if (start === undefined) {
350 start = 0;
351 }
352
353 for (let serial = start, asc = start <= 1000; asc ? serial <= 1000 : serial >= 1000; asc ? serial++ : serial--) {
354 const uri = this.Uri(`${base}${serial}`);
355 if (!this.contains(uri, null, null)) {
356 results.push(uri);
357 log("nextNumberedResources", `picked ${uri}`);
358 this.nextNumber.set(base, serial + 1);
359 if (results.length >= howMany) {
360 return results;
361 }
362 }
363 }
364 throw new Error(`can't make sequential uri with base ${base}`);
365 }
366
367 nextNumberedResource(base: any) {
368 return this.nextNumberedResources(base, 1)[0];
369 }
370
371 contextsWithPattern(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null): N3.NamedNode[] {
372 this.autoDeps.askedFor(s, p, o, null);
373 const ctxs: N3.NamedNode[] = [];
374 for (let q of Array.from(this.graph.getQuads(s, p, o, null))) {
375 if (q.graph.termType != "NamedNode") throw `context was ${q.graph.id}`;
376 ctxs.push(q.graph);
377 }
378 return unique(ctxs);
379 }
380
381 sortKey(uri: N3.NamedNode) {
382 const parts = uri.value.split(/([0-9]+)/);
383 const expanded = parts.map(function (p: string) {
384 const f = parseInt(p);
385 if (isNaN(f)) {
386 return p;
387 }
388 return p.padStart(8, "0");
389 });
390 return expanded.join("");
391 }
392
393 sortedUris(uris: any) {
394 return sortBy(uris, this.sortKey);
395 }
396
397 prettyLiteral(x: any) {
398 if (typeof x === "number") {
399 return this.LiteralRoundedFloat(x);
400 } else {
401 return this.Literal(x);
402 }
403 }
404 }