@shapething/localstore 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # LocalStore
2
+
3
+ A RDF/js Store that reads relative turtle files from disk and mutates them via a QueryEngine such as Comunica.
4
+
5
+ ## How to use:
6
+
7
+ ```TypeScript
8
+
9
+ import { LocalStore } from '@shapething/localstore'
10
+ import { QueryEngine } from '@comunica/query-sparql'
11
+
12
+ const store = new LocalStore({ baseUri: new URL('http://example.com/') })
13
+ const engine = new QueryEngine()
14
+ await engine.queryQuads(
15
+ `construct { ?s ?p ?o } where {
16
+ { graph <http://example.com/nested/lorem> { ?s ?p ?o } } union
17
+ { graph <https://shapething.com/lorem> { ?s ?p ?o } } union
18
+ { graph <http://example.com/ipsum> { ?s ?p ?o } }
19
+ }`,
20
+ {
21
+ sources: [store]
22
+ }
23
+ )
24
+ ```
25
+
26
+ It is best to execute queries with graphs selected.
27
+ This gives the best performance.
@@ -0,0 +1,286 @@
1
+ import f from "@rdfjs/data-model";
2
+ import m from "events";
3
+ import v from "hex-encoding";
4
+ import { get as y, set as E } from "idb-keyval";
5
+ import { Store as w, Parser as S, Writer as N } from "n3";
6
+ import { Readable as T } from "readable-stream";
7
+ async function* x(a, t = ".", e = []) {
8
+ for await (const n of a.values())
9
+ if (n.kind === "file" && n.name.endsWith(".ttl"))
10
+ yield [`${t}/${n.name}`, n];
11
+ else if (n.kind === "directory") {
12
+ if (e.includes(n.name)) continue;
13
+ for await (const [r, s] of x(n, `${t}/${n.name}`, e))
14
+ yield [r, s];
15
+ }
16
+ }
17
+ function $(a) {
18
+ return a && a !== null && a !== void 0;
19
+ }
20
+ const P = async (a, t, e) => {
21
+ const n = a.split("/").filter($);
22
+ let r = n.shift(), s = t;
23
+ for (; s && r; ) {
24
+ try {
25
+ n.length > 0 ? s = await s.getDirectoryHandle(r, { create: e }) : s = await s.getFileHandle(r, { create: e });
26
+ } catch {
27
+ try {
28
+ s = await s.getFileHandle(r, { create: e });
29
+ } catch {
30
+ }
31
+ }
32
+ r = n.shift();
33
+ }
34
+ return s;
35
+ }, G = async (a, t, e) => {
36
+ const n = await P(a, t, e);
37
+ if (n.kind === "file")
38
+ return n;
39
+ }, b = (a) => f.quad(a.subject, a.predicate, a.object);
40
+ class D {
41
+ prefixes = {};
42
+ #t;
43
+ #n;
44
+ #e = new w();
45
+ #r = /* @__PURE__ */ new Map();
46
+ constructor({ baseUri: t }) {
47
+ if (this.#n = t, !t.toString().endsWith("/")) throw new Error("BaseIRI must end on a trailing slash");
48
+ }
49
+ /**
50
+ * Connects to a previously mounted folder when given its name,
51
+ * or show a directory picker to connect to a folder.
52
+ */
53
+ async mount(t) {
54
+ try {
55
+ const e = await y("mounts") ?? /* @__PURE__ */ new Set();
56
+ if (t && e.has(t)) {
57
+ this.#t = await y(t);
58
+ return;
59
+ }
60
+ if (t && !e.has(t) && console.info(`Could not find connection to folder ${t}, please connect again.`), this.#t = await globalThis.showDirectoryPicker(), !this.#t) throw new Error("Could not save the folder");
61
+ await E(t ?? this.#t.name, this.#t), e.add(t ?? this.#t.name), E("mounts", e);
62
+ } catch (e) {
63
+ console.error(e);
64
+ }
65
+ }
66
+ /**
67
+ * Deletes a connection to a folder.
68
+ * This does not delete the folder on disk, but only the connection to it.
69
+ */
70
+ async unmount(t) {
71
+ try {
72
+ if (this.#t) {
73
+ this.#t = void 0;
74
+ const e = await y("mounts") ?? /* @__PURE__ */ new Set();
75
+ e.delete(t), E("mounts", e);
76
+ }
77
+ } catch (e) {
78
+ console.error(e);
79
+ }
80
+ }
81
+ /**
82
+ * Returns the name of the currently mounted folder.
83
+ */
84
+ getFolderName() {
85
+ return this.#t?.name;
86
+ }
87
+ /**
88
+ * Only use this when there is no data yet in the local folder attached to the previous baseURI.
89
+ * Currently this does not rename previous data, it orphans it.
90
+ */
91
+ setBaseUri(t) {
92
+ if (!t.toString().endsWith("/")) throw new Error("BaseURI must end on a trailing slash");
93
+ this.#n = t;
94
+ }
95
+ /**
96
+ * Removes quads by matching
97
+ */
98
+ removeMatches(t, e, n, r) {
99
+ const s = new m();
100
+ let i = 0, o = 0;
101
+ return (async () => {
102
+ const l = r ? [r] : this.getNamedGraphs();
103
+ for await (const c of l) {
104
+ i++, this.#e.removeMatches(t, e, n, c);
105
+ const h = this.match(t, e, n, c), d = [];
106
+ h.on("data", (u) => d.push(u)), h.on("end", () => {
107
+ this.updateGraph(c, { deletions: d }), o++, i === o && s.emit("end");
108
+ });
109
+ }
110
+ })(), s;
111
+ }
112
+ /**
113
+ * Deletes one graph, triggered by DROP GRAPH <a>
114
+ */
115
+ deleteGraph(t) {
116
+ const e = new m(), n = typeof t == "string" ? f.namedNode(t) : t;
117
+ return this.#e.deleteGraph(t), this.#s(n).then(async (r) => {
118
+ r?.remove(), e.emit("end");
119
+ }), e;
120
+ }
121
+ /**
122
+ * Imports quads to one or multiple disk files.
123
+ */
124
+ import(t) {
125
+ const e = new m();
126
+ let n;
127
+ const r = {}, s = (i) => {
128
+ n && (!i || !i.graph.equals(n)) && this.updateGraph(n, { insertions: r[n.value] }), i ? (i.graph.value in r || (r[i.graph.value] = []), r[i.graph.value].push(i), n = i.graph) : (n = void 0, e.emit("end"));
129
+ };
130
+ return t.on("data", s), t.on("end", s), t.on("error", (i) => {
131
+ console.error("Error in input stream:", i), e.emit("error", i);
132
+ }), e;
133
+ }
134
+ /**
135
+ * Removes quads from one or multiple disk files.
136
+ */
137
+ remove(t) {
138
+ const e = new m();
139
+ let n;
140
+ const r = {}, s = (i) => {
141
+ n && (!i || !i.graph.equals(n)) && this.updateGraph(n, { deletions: r[n.value] }), i ? (i.graph.value in r || (r[i.graph.value] = []), r[i.graph.value].push(i), n = i.graph) : (n = void 0, e.emit("end"));
142
+ };
143
+ return t.on("data", s), t.on("end", s), t.on("error", (i) => {
144
+ console.error("Error in input stream:", i), e.emit("error", i);
145
+ }), e;
146
+ }
147
+ /**
148
+ * Updates one graph on disk
149
+ */
150
+ async updateGraph(t, e) {
151
+ const n = await this.#s(t, !0), s = await (await n.getFile()).text(), o = new S({ baseIRI: t.value }).parse(s), l = new w(o), c = new w(e.deletions?.map(b)), h = new w(e.insertions?.map(b)), d = l.difference(c).union(h), u = new N({ baseIRI: t.value });
152
+ return u.addQuads([...d]), new Promise((p, g) => {
153
+ u.end(async (I, F) => {
154
+ const H = await n.createWritable();
155
+ await H.write(F), await H.close(), this.#e.deleteGraph(t), this.#r.delete(t.value), I && g(I), p(void 0);
156
+ });
157
+ });
158
+ }
159
+ /**
160
+ * An optimization for Comunica, so that there are less .match() calls.
161
+ */
162
+ countQuads(t, e, n, r) {
163
+ return r && this.#i(r) ? this.#e.countQuads(t, e, n, r) : 1;
164
+ }
165
+ /**
166
+ * The main function for local store for reading.
167
+ * When a match is done we decide which graphs we need to cache and parse them and put them in the N3 store.
168
+ */
169
+ match(t, e, n, r) {
170
+ if (!this.#t) throw new Error("Local store not mounted");
171
+ const s = new T({ objectMode: !0 });
172
+ let i = !1, o = !1, l = !1;
173
+ return s._read = async () => {
174
+ if (!(i || o)) {
175
+ i = !0;
176
+ try {
177
+ const c = this.getNamedGraphs(r ? [r] : void 0);
178
+ let h = 0, d = 0;
179
+ for await (const u of c) {
180
+ if (o) return;
181
+ h++, this.#i(u) || await this.#o(u);
182
+ const p = this.#e.match(
183
+ /** @ts-expect-error the N3 Store#match typings don't accept nullable term filters here */
184
+ t,
185
+ e,
186
+ n,
187
+ u
188
+ );
189
+ p.on("data", (g) => {
190
+ s.destroyed || o || s.push(g);
191
+ }), p.on("end", () => {
192
+ d++, h === d && !o && l && (o = !0, s.push(null));
193
+ });
194
+ }
195
+ l = !0, !o && l && h === d && (o = !0, s.push(null));
196
+ } catch (c) {
197
+ o || (o = !0, s.destroy(c));
198
+ }
199
+ }
200
+ }, s;
201
+ }
202
+ /**
203
+ * Check in the N3 store if the graph is cached.
204
+ */
205
+ #i(t) {
206
+ return this.#e._graphs[this.#e._termToNumericId(t)] !== void 0;
207
+ }
208
+ /**
209
+ * Cache a graph, it can be the case that a graph on disk is parsed and that this takes a while and in the same time a request is done for the same graph.
210
+ * For this reason we deduplicate the in flight promises.
211
+ */
212
+ async #o(t) {
213
+ const e = await this.#s(t);
214
+ if (e) {
215
+ if (!this.#r.has(t.value)) {
216
+ const n = this.#a(t, e);
217
+ this.#r.set(t.value, n);
218
+ }
219
+ return this.#r.get(t.value);
220
+ }
221
+ }
222
+ /**
223
+ * The actual function that parses a file on disk and puts it into the N3 store.
224
+ */
225
+ async #a(t, e) {
226
+ try {
227
+ const n = new S({ baseIRI: t.value }), s = (await (await e.getFile()).text()).replace(/<(.|\/)(.*)\.ttl(.*)>/g, "<$1$2>"), i = await n.parse(s), o = n._prefixes;
228
+ this.prefixes = { ...this.prefixes, ...o }, this.#e.addQuads(i.map((l) => f.quad(l.subject, l.predicate, l.object, t)));
229
+ } catch (n) {
230
+ throw new Error(`Error while parsing graph ${t.value}:` + (n instanceof Error ? n.message : String(n)));
231
+ }
232
+ }
233
+ /**
234
+ * Returns all named graphs or a subset of them.
235
+ */
236
+ async *getNamedGraphs(t) {
237
+ if (t && t.length > 0) {
238
+ for (const e of t)
239
+ await this.#s(e, !1) && (yield e);
240
+ return;
241
+ }
242
+ for await (const [e] of this.#l())
243
+ yield e;
244
+ }
245
+ async *#l() {
246
+ if (!this.#t) throw new Error("Local store not mounted");
247
+ for await (const [t, e] of x(this.#t))
248
+ yield [this.#c(t), e];
249
+ }
250
+ /**
251
+ * Given a path and a file handle, creates a graph term.
252
+ */
253
+ #c(t) {
254
+ const e = t.substring(0, t.length - 4), r = e.split("/").pop();
255
+ if (t === "default-graph.ttl") return f.defaultGraph();
256
+ if (v.is(r)) {
257
+ const s = v.decodeStr(r);
258
+ return f.namedNode(s);
259
+ }
260
+ return f.namedNode(new URL(e, this.#n).toString());
261
+ }
262
+ /**
263
+ * Given a graph term returns the file handle from disk.
264
+ */
265
+ async #s(t, e) {
266
+ if (!this.#t) throw new Error("Local store not mounted");
267
+ if (t.value.startsWith(this.#n.toString())) {
268
+ const n = t.value.replace(this.#n.toString(), ""), r = n.endsWith("/") ? n.substring(0, n.length - 1) : n, s = `${r}.ttl`;
269
+ if (!r) throw new Error("A graph must have a filename");
270
+ try {
271
+ return G(s, this.#t, e);
272
+ } catch {
273
+ }
274
+ } else {
275
+ if (t.termType === "DefaultGraph")
276
+ return G("default-graph.ttl", this.#t, e);
277
+ {
278
+ const n = new TextEncoder().encode(t.value), s = `${v.encode(n)}.ttl`;
279
+ return G(s, this.#t, e);
280
+ }
281
+ }
282
+ }
283
+ }
284
+ export {
285
+ D as LocalStore
286
+ };
@@ -0,0 +1,70 @@
1
+ import type { DefaultGraph, NamedNode, Quad, Quad_Graph, Store as RdfJsStore, Source, Stream, Term } from '@rdfjs/types';
2
+ import EventEmitter from 'events';
3
+ type LocalStoreOptions = {
4
+ baseUri: URL;
5
+ };
6
+ /**
7
+ * Creates a queryable store, that mounts a directory on the drive and read and writes turtle files.
8
+ * Each of these files can have relative IRIs such as <> or <#lorem> or </ipsum>.
9
+ */
10
+ export declare class LocalStore implements Source, RdfJsStore {
11
+ #private;
12
+ prefixes: Record<string, string>;
13
+ constructor({ baseUri }: LocalStoreOptions);
14
+ /**
15
+ * Connects to a previously mounted folder when given its name,
16
+ * or show a directory picker to connect to a folder.
17
+ */
18
+ mount(name?: string): Promise<void>;
19
+ /**
20
+ * Deletes a connection to a folder.
21
+ * This does not delete the folder on disk, but only the connection to it.
22
+ */
23
+ unmount(name: string): Promise<void>;
24
+ /**
25
+ * Returns the name of the currently mounted folder.
26
+ */
27
+ getFolderName(): string | undefined;
28
+ /**
29
+ * Only use this when there is no data yet in the local folder attached to the previous baseURI.
30
+ * Currently this does not rename previous data, it orphans it.
31
+ */
32
+ setBaseUri(baseUri: URL): void;
33
+ /**
34
+ * Removes quads by matching
35
+ */
36
+ removeMatches(subject?: Term | null, predicate?: Term | null, object?: Term | null, graph?: Term | null): EventEmitter;
37
+ /**
38
+ * Deletes one graph, triggered by DROP GRAPH <a>
39
+ */
40
+ deleteGraph(graph: string | Quad_Graph): EventEmitter;
41
+ /**
42
+ * Imports quads to one or multiple disk files.
43
+ */
44
+ import(stream: Stream<Quad>): EventEmitter;
45
+ /**
46
+ * Removes quads from one or multiple disk files.
47
+ */
48
+ remove(stream: Stream<Quad>): EventEmitter;
49
+ /**
50
+ * Updates one graph on disk
51
+ */
52
+ updateGraph(graph: NamedNode | DefaultGraph, update: {
53
+ deletions?: Quad[];
54
+ insertions?: Quad[];
55
+ }): Promise<void>;
56
+ /**
57
+ * An optimization for Comunica, so that there are less .match() calls.
58
+ */
59
+ countQuads(subject?: Term | null, predicate?: Term | null, object?: Term | null, graph?: Term | null): number;
60
+ /**
61
+ * The main function for local store for reading.
62
+ * When a match is done we decide which graphs we need to cache and parse them and put them in the N3 store.
63
+ */
64
+ match(subject?: Term | null, predicate?: Term | null, object?: Term | null, graph?: Term | null): Stream<Quad>;
65
+ /**
66
+ * Returns all named graphs or a subset of them.
67
+ */
68
+ getNamedGraphs(graphs?: (NamedNode | DefaultGraph)[]): AsyncIterable<NamedNode | DefaultGraph>;
69
+ }
70
+ export {};
@@ -0,0 +1 @@
1
+ export declare function getAllFilesFromDirectory(handle: FileSystemDirectoryHandle, parentPath?: string, skipList?: string[]): AsyncIterable<[string, FileSystemFileHandle]>;
@@ -0,0 +1,2 @@
1
+ export declare const getFileHandleByPath: (path: string, root: FileSystemDirectoryHandle, create?: boolean) => Promise<FileSystemFileHandle | undefined>;
2
+ export declare const getDirectoryHandleByPath: (path: string, root: FileSystemDirectoryHandle, create?: boolean) => Promise<FileSystemDirectoryHandle>;
@@ -0,0 +1 @@
1
+ export declare function nonNullable<T>(value: T): value is NonNullable<T>;
@@ -0,0 +1,2 @@
1
+ import type { Quad } from '@rdfjs/types';
2
+ export declare const toTriple: (quad: Quad) => Quad;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@shapething/localstore",
3
+ "version": "0.1.14",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "main": "./dist/LocalStore.js",
7
+ "types": "./dist/lib/LocalStore.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/lib/LocalStore.d.ts",
11
+ "default": "./dist/LocalStore.js"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": ["dist", "README.md"],
18
+ "scripts": {
19
+ "dev": "vite",
20
+ "build": "vite build --config vite.lib.config.ts && tsc -p tsconfig.build.json",
21
+ "lint": "eslint .",
22
+ "test": "vitest run --passWithNoTests",
23
+ "test:watch": "vitest"
24
+ },
25
+ "dependencies": {
26
+ "@rdfjs/data-model": "^2.1.0",
27
+ "events": "^3.3.0",
28
+ "hex-encoding": "^2.0.3",
29
+ "idb-keyval": "^6.2.2",
30
+ "n3": "^1.25.2",
31
+ "readable-stream": "^4.7.0"
32
+ },
33
+ "devDependencies": {
34
+ "@comunica/query-sparql": "^4.2.0",
35
+ "@rdfjs/types": "^2.0.1",
36
+ "@testing-library/dom": "^10.4.0",
37
+ "@types/events": "^3.0.3",
38
+ "@types/n3": "^1.25.3",
39
+ "@types/node": "^22.15.29",
40
+ "@types/rdfjs__data-model": "^2.0.9",
41
+ "@types/wicg-file-system-access": "^2023.10.6",
42
+ "@vitest/browser": "^3.1.4",
43
+ "typescript": "~5.8.3",
44
+ "vite": "^6.3.5",
45
+ "vitest": "^3.1.4"
46
+ }
47
+ }