eyeling 1.28.9 → 1.29.0
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 +60 -1
- package/dist/browser/eyeling.browser.js +915 -0
- package/dist/browser/index.mjs +10 -0
- package/eyeling.js +915 -0
- package/index.d.ts +59 -0
- package/index.js +17 -0
- package/lib/cli.js +102 -0
- package/lib/engine.js +120 -0
- package/lib/entry.js +4 -0
- package/lib/rdfjs.js +1 -0
- package/lib/store.js +685 -0
- package/package.json +8 -3
- package/test/store.test.js +138 -0
- package/tools/bundle.js +10 -0
package/lib/store.js
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eyeling Reasoner — fact stores
|
|
3
|
+
*
|
|
4
|
+
* Optional async fact-store abstraction used by runAsync() and by tests. The
|
|
5
|
+
* in-memory reasoner remains the default; persistent stores are opt-in.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
Iri,
|
|
12
|
+
Literal,
|
|
13
|
+
Var,
|
|
14
|
+
Blank,
|
|
15
|
+
ListTerm,
|
|
16
|
+
OpenListTerm,
|
|
17
|
+
GraphTerm,
|
|
18
|
+
Triple,
|
|
19
|
+
} = require('./prelude');
|
|
20
|
+
|
|
21
|
+
const KIND_EXPLICIT = 1;
|
|
22
|
+
const KIND_INFERRED = 2;
|
|
23
|
+
|
|
24
|
+
function __dynamicRequire(name) {
|
|
25
|
+
try {
|
|
26
|
+
// Keep this indirect so the browser bundle can include this module without
|
|
27
|
+
// eagerly trying to bundle Node-only or optional Level dependencies.
|
|
28
|
+
const req = typeof require === 'function' ? require : null;
|
|
29
|
+
return req ? req(name) : null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function base64url(s) {
|
|
36
|
+
const text = String(s);
|
|
37
|
+
if (typeof Buffer !== 'undefined') return Buffer.from(text, 'utf8').toString('base64url');
|
|
38
|
+
const encoder = globalThis.TextEncoder ? new globalThis.TextEncoder() : null;
|
|
39
|
+
if (!encoder || typeof globalThis.btoa !== 'function') {
|
|
40
|
+
throw new Error('No UTF-8/base64 encoder available for persistent store keys');
|
|
41
|
+
}
|
|
42
|
+
const bytes = encoder.encode(text);
|
|
43
|
+
let bin = '';
|
|
44
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
45
|
+
return globalThis.btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function idPart(id) {
|
|
49
|
+
return String(id).padStart(16, '0');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function safeStoreName(name) {
|
|
53
|
+
const text = String(name || 'default');
|
|
54
|
+
return encodeURIComponent(text).replace(/%/g, '_');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function termToJson(term) {
|
|
58
|
+
if (term instanceof Iri) return ['Iri', term.value];
|
|
59
|
+
if (term instanceof Literal) return ['Literal', term.value];
|
|
60
|
+
if (term instanceof Var) return ['Var', term.name];
|
|
61
|
+
if (term instanceof Blank) return ['Blank', term.label];
|
|
62
|
+
if (term instanceof ListTerm) return ['ListTerm', term.elems.map(termToJson)];
|
|
63
|
+
if (term instanceof OpenListTerm) return ['OpenListTerm', term.prefix.map(termToJson), term.tailVar];
|
|
64
|
+
if (term instanceof GraphTerm) return ['GraphTerm', term.triples.map(tripleToJson)];
|
|
65
|
+
|
|
66
|
+
// RDF/JS terms, for callers that use the store abstraction directly.
|
|
67
|
+
if (term && typeof term === 'object' && typeof term.termType === 'string') {
|
|
68
|
+
switch (term.termType) {
|
|
69
|
+
case 'NamedNode':
|
|
70
|
+
return ['Iri', term.value];
|
|
71
|
+
case 'BlankNode':
|
|
72
|
+
return ['Blank', term.value && term.value.startsWith('_:') ? term.value : `_:${term.value}`];
|
|
73
|
+
case 'Variable':
|
|
74
|
+
return ['Var', term.value];
|
|
75
|
+
case 'Literal': {
|
|
76
|
+
const dt = term.datatype && term.datatype.termType === 'NamedNode' ? term.datatype.value : '';
|
|
77
|
+
const lang = typeof term.language === 'string' ? term.language : '';
|
|
78
|
+
const q = JSON.stringify(String(term.value));
|
|
79
|
+
if (lang) return ['Literal', `${q}@${lang}`];
|
|
80
|
+
if (!dt || dt === 'http://www.w3.org/2001/XMLSchema#string') return ['Literal', q];
|
|
81
|
+
return ['Literal', `${q}^^<${dt}>`];
|
|
82
|
+
}
|
|
83
|
+
case 'Quad':
|
|
84
|
+
return ['GraphTerm', [tripleToJson({ s: term.subject, p: term.predicate, o: term.object })]];
|
|
85
|
+
case 'DefaultGraph':
|
|
86
|
+
return ['DefaultGraph'];
|
|
87
|
+
default:
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new TypeError(`Unsupported fact-store term: ${term && term.constructor ? term.constructor.name : String(term)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function termFromJson(json) {
|
|
96
|
+
if (!Array.isArray(json)) throw new TypeError('Invalid serialized term');
|
|
97
|
+
switch (json[0]) {
|
|
98
|
+
case 'Iri':
|
|
99
|
+
return new Iri(json[1]);
|
|
100
|
+
case 'Literal':
|
|
101
|
+
return new Literal(json[1]);
|
|
102
|
+
case 'Var':
|
|
103
|
+
return new Var(json[1]);
|
|
104
|
+
case 'Blank':
|
|
105
|
+
return new Blank(json[1]);
|
|
106
|
+
case 'ListTerm':
|
|
107
|
+
return new ListTerm((json[1] || []).map(termFromJson));
|
|
108
|
+
case 'OpenListTerm':
|
|
109
|
+
return new OpenListTerm((json[1] || []).map(termFromJson), json[2]);
|
|
110
|
+
case 'GraphTerm':
|
|
111
|
+
return new GraphTerm((json[1] || []).map(tripleFromJson));
|
|
112
|
+
case 'DefaultGraph':
|
|
113
|
+
return new Iri('urn:eyeling:default-graph');
|
|
114
|
+
default:
|
|
115
|
+
throw new TypeError(`Unsupported serialized term type: ${json[0]}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tripleToJson(triple) {
|
|
120
|
+
return [termToJson(triple.s || triple.subject), termToJson(triple.p || triple.predicate), termToJson(triple.o || triple.object)];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function tripleFromJson(json) {
|
|
124
|
+
return new Triple(termFromJson(json[0]), termFromJson(json[1]), termFromJson(json[2]));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function termToStoreKey(term) {
|
|
128
|
+
return JSON.stringify(termToJson(term));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function tripleToStoreKey(triple) {
|
|
132
|
+
return JSON.stringify(tripleToJson(triple));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function kindMask(kind) {
|
|
136
|
+
if (kind === 'explicit') return KIND_EXPLICIT;
|
|
137
|
+
if (kind === 'inferred') return KIND_INFERRED;
|
|
138
|
+
if (typeof kind === 'number') return kind & (KIND_EXPLICIT | KIND_INFERRED);
|
|
139
|
+
return KIND_EXPLICIT;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class MemoryKv {
|
|
143
|
+
constructor() {
|
|
144
|
+
this.map = new Map();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async get(key) {
|
|
148
|
+
return this.map.get(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async put(key, value) {
|
|
152
|
+
this.map.set(key, value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async del(key) {
|
|
156
|
+
this.map.delete(key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async clear() {
|
|
160
|
+
this.map.clear();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async batch(ops) {
|
|
164
|
+
for (const op of ops || []) {
|
|
165
|
+
if (!op) continue;
|
|
166
|
+
if (op.type === 'del') this.map.delete(op.key);
|
|
167
|
+
else this.map.set(op.key, op.value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async *entries(prefix = '') {
|
|
172
|
+
const keys = Array.from(this.map.keys())
|
|
173
|
+
.filter((key) => key.startsWith(prefix))
|
|
174
|
+
.sort();
|
|
175
|
+
for (const key of keys) yield [key, this.map.get(key)];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async close() {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
class JsonFileKv extends MemoryKv {
|
|
182
|
+
constructor(filePath) {
|
|
183
|
+
super();
|
|
184
|
+
this.filePath = filePath;
|
|
185
|
+
this.loaded = false;
|
|
186
|
+
this.dirty = false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async open() {
|
|
190
|
+
if (this.loaded) return;
|
|
191
|
+
this.loaded = true;
|
|
192
|
+
const fs = __dynamicRequire('node:fs');
|
|
193
|
+
const path = __dynamicRequire('node:path');
|
|
194
|
+
if (!fs || !path) throw new Error('Persistent stores need node:fs in this runtime');
|
|
195
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
196
|
+
if (!fs.existsSync(this.filePath)) return;
|
|
197
|
+
const text = fs.readFileSync(this.filePath, 'utf8');
|
|
198
|
+
if (!text.trim()) return;
|
|
199
|
+
const data = JSON.parse(text);
|
|
200
|
+
this.map = new Map(Array.isArray(data.entries) ? data.entries : []);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async flush() {
|
|
204
|
+
if (!this.loaded || !this.dirty) return;
|
|
205
|
+
const fs = __dynamicRequire('node:fs');
|
|
206
|
+
const path = __dynamicRequire('node:path');
|
|
207
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
208
|
+
const tmp = `${this.filePath}.tmp`;
|
|
209
|
+
fs.writeFileSync(tmp, JSON.stringify({ entries: Array.from(this.map.entries()) }), 'utf8');
|
|
210
|
+
fs.renameSync(tmp, this.filePath);
|
|
211
|
+
this.dirty = false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async get(key) {
|
|
215
|
+
await this.open();
|
|
216
|
+
return super.get(key);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async put(key, value) {
|
|
220
|
+
await this.open();
|
|
221
|
+
await super.put(key, value);
|
|
222
|
+
this.dirty = true;
|
|
223
|
+
await this.flush();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async del(key) {
|
|
227
|
+
await this.open();
|
|
228
|
+
await super.del(key);
|
|
229
|
+
this.dirty = true;
|
|
230
|
+
await this.flush();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async clear() {
|
|
234
|
+
await this.open();
|
|
235
|
+
await super.clear();
|
|
236
|
+
this.dirty = true;
|
|
237
|
+
await this.flush();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async batch(ops) {
|
|
241
|
+
await this.open();
|
|
242
|
+
await super.batch(ops);
|
|
243
|
+
this.dirty = true;
|
|
244
|
+
await this.flush();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async *entries(prefix = '') {
|
|
248
|
+
await this.open();
|
|
249
|
+
yield* super.entries(prefix);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async close() {
|
|
253
|
+
await this.flush();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
class ClassicLevelKv {
|
|
258
|
+
constructor(location) {
|
|
259
|
+
const classic = __dynamicRequire('classic-level');
|
|
260
|
+
const ClassicLevel = classic && (classic.ClassicLevel || classic.Level || classic.default);
|
|
261
|
+
if (!ClassicLevel) throw new Error('classic-level is not installed');
|
|
262
|
+
this.db = new ClassicLevel(location, { valueEncoding: 'json' });
|
|
263
|
+
this.opened = false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async open() {
|
|
267
|
+
if (this.opened) return;
|
|
268
|
+
if (typeof this.db.open === 'function') await this.db.open();
|
|
269
|
+
this.opened = true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async get(key) {
|
|
273
|
+
await this.open();
|
|
274
|
+
try {
|
|
275
|
+
return await this.db.get(key);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
if (e && (e.notFound || e.code === 'LEVEL_NOT_FOUND' || e.code === 'NOT_FOUND')) return undefined;
|
|
278
|
+
throw e;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async put(key, value) {
|
|
283
|
+
await this.open();
|
|
284
|
+
await this.db.put(key, value);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async del(key) {
|
|
288
|
+
await this.open();
|
|
289
|
+
await this.db.del(key);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async clear() {
|
|
293
|
+
await this.open();
|
|
294
|
+
if (typeof this.db.clear === 'function') await this.db.clear();
|
|
295
|
+
else {
|
|
296
|
+
const ops = [];
|
|
297
|
+
for await (const [key] of this.entries('')) ops.push({ type: 'del', key });
|
|
298
|
+
if (ops.length) await this.batch(ops);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async batch(ops) {
|
|
303
|
+
await this.open();
|
|
304
|
+
if (!ops || !ops.length) return;
|
|
305
|
+
await this.db.batch(ops);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async *entries(prefix = '') {
|
|
309
|
+
await this.open();
|
|
310
|
+
const gte = prefix;
|
|
311
|
+
const lt = prefix + '\uffff';
|
|
312
|
+
const iterator = this.db.iterator({ gte, lt });
|
|
313
|
+
for await (const item of iterator) {
|
|
314
|
+
if (Array.isArray(item)) yield item;
|
|
315
|
+
else if (item && Array.isArray(item.key)) yield item.key;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async close() {
|
|
320
|
+
if (this.db && typeof this.db.close === 'function') await this.db.close();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
class IndexedDbKv {
|
|
325
|
+
constructor(name) {
|
|
326
|
+
this.name = name;
|
|
327
|
+
this.dbPromise = null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
open() {
|
|
331
|
+
if (this.dbPromise) return this.dbPromise;
|
|
332
|
+
const idb = typeof globalThis !== 'undefined' ? globalThis.indexedDB : null;
|
|
333
|
+
if (!idb) throw new Error('IndexedDB is not available in this runtime');
|
|
334
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
335
|
+
const req = idb.open(`eyeling:${this.name}`, 1);
|
|
336
|
+
req.onupgradeneeded = () => {
|
|
337
|
+
const db = req.result;
|
|
338
|
+
if (!db.objectStoreNames.contains('kv')) db.createObjectStore('kv');
|
|
339
|
+
};
|
|
340
|
+
req.onerror = () => reject(req.error || new Error('Failed to open IndexedDB store'));
|
|
341
|
+
req.onsuccess = () => resolve(req.result);
|
|
342
|
+
});
|
|
343
|
+
return this.dbPromise;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async __tx(mode, fn) {
|
|
347
|
+
const db = await this.open();
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
const tx = db.transaction('kv', mode);
|
|
350
|
+
const store = tx.objectStore('kv');
|
|
351
|
+
let value;
|
|
352
|
+
tx.oncomplete = () => resolve(value);
|
|
353
|
+
tx.onerror = () => reject(tx.error || new Error('IndexedDB transaction failed'));
|
|
354
|
+
tx.onabort = () => reject(tx.error || new Error('IndexedDB transaction aborted'));
|
|
355
|
+
try {
|
|
356
|
+
value = fn(store, resolve, reject);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
try { tx.abort(); } catch {}
|
|
359
|
+
reject(e);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async get(key) {
|
|
365
|
+
return this.__tx('readonly', (store, resolve, reject) => {
|
|
366
|
+
const req = store.get(key);
|
|
367
|
+
req.onerror = () => reject(req.error);
|
|
368
|
+
req.onsuccess = () => resolve(req.result);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async put(key, value) {
|
|
373
|
+
return this.__tx('readwrite', (store) => {
|
|
374
|
+
store.put(value, key);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async del(key) {
|
|
379
|
+
return this.__tx('readwrite', (store) => {
|
|
380
|
+
store.delete(key);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async clear() {
|
|
385
|
+
return this.__tx('readwrite', (store) => {
|
|
386
|
+
store.clear();
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async batch(ops) {
|
|
391
|
+
return this.__tx('readwrite', (store) => {
|
|
392
|
+
for (const op of ops || []) {
|
|
393
|
+
if (op.type === 'del') store.delete(op.key);
|
|
394
|
+
else store.put(op.value, op.key);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async *entries(prefix = '') {
|
|
400
|
+
const db = await this.open();
|
|
401
|
+
const rows = await new Promise((resolve, reject) => {
|
|
402
|
+
const tx = db.transaction('kv', 'readonly');
|
|
403
|
+
const store = tx.objectStore('kv');
|
|
404
|
+
const out = [];
|
|
405
|
+
const range = globalThis.IDBKeyRange.bound(prefix, prefix + '\uffff', false, true);
|
|
406
|
+
const req = store.openCursor(range);
|
|
407
|
+
req.onerror = () => reject(req.error);
|
|
408
|
+
req.onsuccess = () => {
|
|
409
|
+
const cursor = req.result;
|
|
410
|
+
if (!cursor) return;
|
|
411
|
+
out.push([cursor.key, cursor.value]);
|
|
412
|
+
cursor.continue();
|
|
413
|
+
};
|
|
414
|
+
tx.oncomplete = () => resolve(out);
|
|
415
|
+
tx.onerror = () => reject(tx.error || new Error('IndexedDB transaction failed'));
|
|
416
|
+
});
|
|
417
|
+
for (const row of rows) yield row;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async close() {
|
|
421
|
+
if (!this.dbPromise) return;
|
|
422
|
+
const db = await this.dbPromise;
|
|
423
|
+
if (db && typeof db.close === 'function') db.close();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
class MemoryFactStore {
|
|
428
|
+
constructor() {
|
|
429
|
+
this.map = new Map();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async add(triple, kind = 'explicit') {
|
|
433
|
+
const key = tripleToStoreKey(triple);
|
|
434
|
+
const prev = this.map.get(key);
|
|
435
|
+
const mask = kindMask(kind);
|
|
436
|
+
if (prev) {
|
|
437
|
+
const nextKind = prev.kind | mask;
|
|
438
|
+
if (nextKind !== prev.kind) prev.kind = nextKind;
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
this.map.set(key, { triple, kind: mask });
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async batchAdd(triples, kind = 'explicit') {
|
|
446
|
+
let n = 0;
|
|
447
|
+
for (const triple of triples || []) if (await this.add(triple, kind)) n += 1;
|
|
448
|
+
return n;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async has(triple) {
|
|
452
|
+
return this.map.has(tripleToStoreKey(triple));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async kindOf(triple) {
|
|
456
|
+
const row = this.map.get(tripleToStoreKey(triple));
|
|
457
|
+
return row ? row.kind : 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async *match(s, p, o) {
|
|
461
|
+
const sk = s == null ? null : termToStoreKey(s);
|
|
462
|
+
const pk = p == null ? null : termToStoreKey(p);
|
|
463
|
+
const ok = o == null ? null : termToStoreKey(o);
|
|
464
|
+
for (const { triple } of this.map.values()) {
|
|
465
|
+
if (sk !== null && termToStoreKey(triple.s) !== sk) continue;
|
|
466
|
+
if (pk !== null && termToStoreKey(triple.p) !== pk) continue;
|
|
467
|
+
if (ok !== null && termToStoreKey(triple.o) !== ok) continue;
|
|
468
|
+
yield triple;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async clear() {
|
|
473
|
+
this.map.clear();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async close() {}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
class PersistentFactStore {
|
|
480
|
+
constructor(kv, options = {}) {
|
|
481
|
+
this.kv = kv;
|
|
482
|
+
this.termCacheByKey = new Map();
|
|
483
|
+
this.termCacheById = new Map();
|
|
484
|
+
this.name = options.name || 'default';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async clear() {
|
|
488
|
+
await this.kv.clear();
|
|
489
|
+
this.termCacheByKey.clear();
|
|
490
|
+
this.termCacheById.clear();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async __nextTermId() {
|
|
494
|
+
const key = 'meta/nextTermId';
|
|
495
|
+
const current = Number((await this.kv.get(key)) || 1);
|
|
496
|
+
await this.kv.put(key, current + 1);
|
|
497
|
+
return idPart(current);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async __idForTerm(term, create) {
|
|
501
|
+
const canonical = termToStoreKey(term);
|
|
502
|
+
if (this.termCacheByKey.has(canonical)) return this.termCacheByKey.get(canonical);
|
|
503
|
+
const byLexKey = `term/byLex/${base64url(canonical)}`;
|
|
504
|
+
const found = await this.kv.get(byLexKey);
|
|
505
|
+
if (found !== undefined) {
|
|
506
|
+
this.termCacheByKey.set(canonical, found);
|
|
507
|
+
return found;
|
|
508
|
+
}
|
|
509
|
+
if (!create) return null;
|
|
510
|
+
const id = await this.__nextTermId();
|
|
511
|
+
const json = termToJson(term);
|
|
512
|
+
await this.kv.batch([
|
|
513
|
+
{ type: 'put', key: byLexKey, value: id },
|
|
514
|
+
{ type: 'put', key: `term/byId/${id}`, value: json },
|
|
515
|
+
]);
|
|
516
|
+
this.termCacheByKey.set(canonical, id);
|
|
517
|
+
this.termCacheById.set(id, term);
|
|
518
|
+
return id;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async __termById(id) {
|
|
522
|
+
if (this.termCacheById.has(id)) return this.termCacheById.get(id);
|
|
523
|
+
const json = await this.kv.get(`term/byId/${id}`);
|
|
524
|
+
if (json === undefined) throw new Error(`Corrupt fact store: missing term ${id}`);
|
|
525
|
+
const term = termFromJson(json);
|
|
526
|
+
this.termCacheById.set(id, term);
|
|
527
|
+
return term;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async __tripleFromIds(sid, pid, oid) {
|
|
531
|
+
return new Triple(await this.__termById(sid), await this.__termById(pid), await this.__termById(oid));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async add(triple, kind = 'explicit') {
|
|
535
|
+
const sid = await this.__idForTerm(triple.s, true);
|
|
536
|
+
const pid = await this.__idForTerm(triple.p, true);
|
|
537
|
+
const oid = await this.__idForTerm(triple.o, true);
|
|
538
|
+
const mask = kindMask(kind);
|
|
539
|
+
const primary = `triple/${sid}/${pid}/${oid}`;
|
|
540
|
+
const prev = await this.kv.get(primary);
|
|
541
|
+
if (prev !== undefined) {
|
|
542
|
+
const nextKind = (typeof prev === 'number' ? prev : prev.kind || 0) | mask;
|
|
543
|
+
if (nextKind !== prev) await this.kv.put(primary, nextKind);
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
await this.kv.batch([
|
|
547
|
+
{ type: 'put', key: primary, value: mask },
|
|
548
|
+
{ type: 'put', key: `i/spo/${sid}/${pid}/${oid}`, value: mask },
|
|
549
|
+
{ type: 'put', key: `i/pos/${pid}/${oid}/${sid}`, value: mask },
|
|
550
|
+
{ type: 'put', key: `i/osp/${oid}/${sid}/${pid}`, value: mask },
|
|
551
|
+
]);
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async batchAdd(triples, kind = 'explicit') {
|
|
556
|
+
let n = 0;
|
|
557
|
+
for (const triple of triples || []) if (await this.add(triple, kind)) n += 1;
|
|
558
|
+
return n;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async has(triple) {
|
|
562
|
+
return (await this.kindOf(triple)) !== 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async kindOf(triple) {
|
|
566
|
+
const sid = await this.__idForTerm(triple.s, false);
|
|
567
|
+
if (sid === null) return 0;
|
|
568
|
+
const pid = await this.__idForTerm(triple.p, false);
|
|
569
|
+
if (pid === null) return 0;
|
|
570
|
+
const oid = await this.__idForTerm(triple.o, false);
|
|
571
|
+
if (oid === null) return 0;
|
|
572
|
+
const value = await this.kv.get(`triple/${sid}/${pid}/${oid}`);
|
|
573
|
+
return typeof value === 'number' ? value : value && typeof value.kind === 'number' ? value.kind : 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async __idsForPattern(s, p, o) {
|
|
577
|
+
const sid = s == null ? null : await this.__idForTerm(s, false);
|
|
578
|
+
const pid = p == null ? null : await this.__idForTerm(p, false);
|
|
579
|
+
const oid = o == null ? null : await this.__idForTerm(o, false);
|
|
580
|
+
if ((s != null && sid === null) || (p != null && pid === null) || (o != null && oid === null)) return null;
|
|
581
|
+
return { sid, pid, oid };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
__scanPlan(sid, pid, oid) {
|
|
585
|
+
if (sid && pid && oid) return { index: 'triple', prefix: `triple/${sid}/${pid}/${oid}`, order: 'spo' };
|
|
586
|
+
if (sid && pid) return { index: 'spo', prefix: `i/spo/${sid}/${pid}/`, order: 'spo' };
|
|
587
|
+
if (pid && oid) return { index: 'pos', prefix: `i/pos/${pid}/${oid}/`, order: 'pos' };
|
|
588
|
+
if (sid && oid) return { index: 'osp', prefix: `i/osp/${oid}/${sid}/`, order: 'osp' };
|
|
589
|
+
if (pid) return { index: 'pos', prefix: `i/pos/${pid}/`, order: 'pos' };
|
|
590
|
+
if (sid) return { index: 'spo', prefix: `i/spo/${sid}/`, order: 'spo' };
|
|
591
|
+
if (oid) return { index: 'osp', prefix: `i/osp/${oid}/`, order: 'osp' };
|
|
592
|
+
return { index: 'spo', prefix: 'i/spo/', order: 'spo' };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
__decodeIndexKey(key, plan) {
|
|
596
|
+
if (plan.index === 'triple') {
|
|
597
|
+
const parts = key.split('/');
|
|
598
|
+
return { sid: parts[1], pid: parts[2], oid: parts[3] };
|
|
599
|
+
}
|
|
600
|
+
const rest = key.slice(`i/${plan.index}/`.length).split('/');
|
|
601
|
+
if (plan.order === 'spo') return { sid: rest[0], pid: rest[1], oid: rest[2] };
|
|
602
|
+
if (plan.order === 'pos') return { pid: rest[0], oid: rest[1], sid: rest[2] };
|
|
603
|
+
return { oid: rest[0], sid: rest[1], pid: rest[2] };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async *match(s, p, o) {
|
|
607
|
+
const ids = await this.__idsForPattern(s, p, o);
|
|
608
|
+
if (!ids) return;
|
|
609
|
+
const plan = this.__scanPlan(ids.sid, ids.pid, ids.oid);
|
|
610
|
+
const seen = new Set();
|
|
611
|
+
for await (const [key] of this.kv.entries(plan.prefix)) {
|
|
612
|
+
const row = this.__decodeIndexKey(key, plan);
|
|
613
|
+
if (ids.sid && row.sid !== ids.sid) continue;
|
|
614
|
+
if (ids.pid && row.pid !== ids.pid) continue;
|
|
615
|
+
if (ids.oid && row.oid !== ids.oid) continue;
|
|
616
|
+
const primary = `${row.sid}/${row.pid}/${row.oid}`;
|
|
617
|
+
if (seen.has(primary)) continue;
|
|
618
|
+
seen.add(primary);
|
|
619
|
+
yield this.__tripleFromIds(row.sid, row.pid, row.oid);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async close() {
|
|
624
|
+
if (this.kv && typeof this.kv.close === 'function') await this.kv.close();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function nodeStoreLocation(name, storePath) {
|
|
629
|
+
const path = __dynamicRequire('node:path');
|
|
630
|
+
const os = __dynamicRequire('node:os');
|
|
631
|
+
if (!path || !os) return null;
|
|
632
|
+
const base = storePath || path.join(os.homedir ? os.homedir() : '.', '.eyeling-store');
|
|
633
|
+
return path.join(base, safeStoreName(name));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function createPersistentFactStore(options = {}) {
|
|
637
|
+
const name = typeof options === 'string' ? options : options.name || 'default';
|
|
638
|
+
const clear = !!(options && options.clear);
|
|
639
|
+
let kv;
|
|
640
|
+
|
|
641
|
+
if (typeof globalThis !== 'undefined' && globalThis.indexedDB && !__dynamicRequire('node:fs')) {
|
|
642
|
+
kv = new IndexedDbKv(name);
|
|
643
|
+
} else {
|
|
644
|
+
const location = nodeStoreLocation(name, options && options.path);
|
|
645
|
+
try {
|
|
646
|
+
kv = new ClassicLevelKv(location);
|
|
647
|
+
} catch {
|
|
648
|
+
kv = new JsonFileKv(`${location}.json`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const store = new PersistentFactStore(kv, { name });
|
|
653
|
+
if (clear) await store.clear();
|
|
654
|
+
return store;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function createFactStore(options = null) {
|
|
658
|
+
if (!options) return new MemoryFactStore();
|
|
659
|
+
if (typeof options === 'string') return createPersistentFactStore({ name: options });
|
|
660
|
+
if (options.type === 'memory') return new MemoryFactStore();
|
|
661
|
+
if (options.backend === 'memory') return new MemoryFactStore();
|
|
662
|
+
return createPersistentFactStore(options);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function collectStore(store) {
|
|
666
|
+
const out = [];
|
|
667
|
+
for await (const tr of store.match(null, null, null)) out.push(tr);
|
|
668
|
+
return out;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
module.exports = {
|
|
672
|
+
KIND_EXPLICIT,
|
|
673
|
+
KIND_INFERRED,
|
|
674
|
+
MemoryFactStore,
|
|
675
|
+
PersistentFactStore,
|
|
676
|
+
createFactStore,
|
|
677
|
+
createPersistentFactStore,
|
|
678
|
+
collectStore,
|
|
679
|
+
termToStoreKey,
|
|
680
|
+
tripleToStoreKey,
|
|
681
|
+
termToJson,
|
|
682
|
+
termFromJson,
|
|
683
|
+
tripleToJson,
|
|
684
|
+
tripleFromJson,
|
|
685
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyeling",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "A minimal Notation3 (N3) reasoner in JavaScript.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -55,10 +55,11 @@
|
|
|
55
55
|
"test:playground": "node test/playground.test.js",
|
|
56
56
|
"test:package": "node test/package.test.js",
|
|
57
57
|
"pretest": "npm run build && npm run test:packlist",
|
|
58
|
-
"test": "npm run test:api && npm run test:builtins && npm run test:examples && npm run test:examples:proof && npm run test:manifest && npm run test:rdf12 && npm run test:playground",
|
|
58
|
+
"test": "npm run test:api && npm run test:builtins && npm run test:store && npm run test:examples && npm run test:examples:proof && npm run test:manifest && npm run test:rdf12 && npm run test:playground",
|
|
59
59
|
"posttest": "npm run test:package",
|
|
60
60
|
"preversion": "npm test",
|
|
61
|
-
"postversion": "git push origin HEAD --follow-tags"
|
|
61
|
+
"postversion": "git push origin HEAD --follow-tags",
|
|
62
|
+
"test:store": "node test/store.test.js"
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"rdf-test-suite": "^2.1.4"
|
|
@@ -83,5 +84,9 @@
|
|
|
83
84
|
"index.d.ts"
|
|
84
85
|
]
|
|
85
86
|
}
|
|
87
|
+
},
|
|
88
|
+
"optionalDependencies": {
|
|
89
|
+
"classic-level": "^3.0.0",
|
|
90
|
+
"browser-level": "^3.0.0"
|
|
86
91
|
}
|
|
87
92
|
}
|